- 投稿日:2019-12-15T20:57:21+09:00
AWS Kinesis Video StreamsでビデオストリーミングWebRTCを実施
こんにちは フリーランスの永田です。
今年も仕事をしながら、技術を探求して、ARのライブラリー、ビデオ編集のライブラリー、背面前面同時撮影できるカメラのライブラリーなどを作成しました。 こちらに技術を公開しています
今回はSwift愛好会の2019年のアドベントカレンダーに参加させていただきました。
AWS Kinesis Video StreamsでビデオストリーミングWebRTCを実施します。
こちらの順序で展開させていただきます。
環境 サンプルソース やり方 つまづきポイント 挙動環境
macOS Catalina 10.16.2
Xcode Version 11.2.1 (11B500)
AWS マネジメントコンソールに入れること(AWSに登録済み)
AWS Kinesis Video Streamsでシグナリングチャンネルの作成
サンプルソース
このリンク先のアプリとAWS Kinesis Video Streamsを連携させてWebRTCを実現します。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-iosやり方
サンプルソースのReadMeを確認願います。
Amazon Cognitoのユーザープール、IDプールの管理設定が必要になります。
Amazon Cognitoの作成方法、サンプルの確認手順はこのリンクに書いています。
AWSKinesisVideoWebRTCDemoApp.xcworkspace
をbuild&Runbuildが通らない場合、サンプルソースにbuildを通すコマンド操作を確認。
記載箇所にAmazon Cognitoで設定した項目を記載します。
Constants.swift
import Foundation import AWSCognitoIdentityProvider // 記入 let CognitoIdentityUserPoolRegion = AWSRegionType.APNortheast1 // 記入 let CognitoIdentityUserPoolId = "Amazon Cognitoのプール ID" // 記入 let CognitoIdentityUserPoolAppClientId = "Amazon Cognitoのアプリクライアント ID" // 記入 let CognitoIdentityUserPoolAppClientSecret = "Amazon Cognitoのアプリクライアントのシークレット" let AWSCognitoUserPoolsSignInProviderKey = "UserPool" // 記入 let CognitoIdentityPoolID = "ap-northeast-1:~~~~~~~~~~~~~ ID プールの管理から作成するID プールID" // 記入 let AWSKinesisVideoEndpoint = "https://kinesisvideo.ap-northeast-1.amazonaws.com" let AWSKinesisVideoKey = "kinesisvideo" let VideoProtocols = ["WSS", "HTTPS"] let ConnectAsMaster = "connect-as-master" let ConnectAsViewer = "connect-as-viewer" let MasterRole = "MASTER" let ViewerRole = "VIEWER" let ClientID = "ConsumerViewer"awsconfiguration.json
{ "Version": "1.0", "CredentialsProvider": { "CognitoIdentity": { "Default": { // 記入 "PoolId": "ap-northeast-1:~~~~~~~~~~~~~ ID プールの管理から作成するID プールID", // 記入 "Region": "ap-northeast-1" } } }, "IdentityManager": { "Default": {} }, "CognitoUserPool": { "Default": { // 記入 "AppClientSecret": "Amazon Cognitoのアプリクライアントのシークレット", // 記入 "AppClientId": "Amazon Cognitoのアプリクライアント ID", // 記入 "PoolId": "Amazon Cognitoのプール ID", // 記入 "Region": "ap-northeast-1" } } }
パスワードを再設定の際、Amazon Cognitoでメール、SMSからパスワードの再設定コードを受け取る必要があります。
- サインインしてConnect画面に遷移
Kinesis Video Streamsのシグナリングチャンネルのチャンネル名
Amazon Cognitoのアプリクライアントのシークレット
リージョン名
を記入して、青い色Button(どちらでも良い)を押下で成功するとConnectedになり、RTC専用のカメラプレビュー画面に遷移します。AWSのライブラリーではカメラもライブラリー内で使用されていて、UIViewを渡す処理になっていますので、カメラの種類をカスタマイズは難しいようです。モバイルで表示しているプログラムです。ライブラリーのRTCMTLVideoViewはUIViewになります。
override func viewDidLoad() { super.viewDidLoad() #if arch(arm64) // Using metal (arm64 only) let localRenderer = RTCMTLVideoView(frame: localVideoView?.frame ?? CGRect.zero) let remoteRenderer = RTCMTLVideoView(frame: view.frame) localRenderer.videoContentMode = .scaleAspectFill remoteRenderer.videoContentMode = .scaleAspectFill #else // Using OpenGLES for the rest let localRenderer = RTCEAGLVideoView(frame: localVideoView?.frame ?? CGRect.zero) let remoteRenderer = RTCEAGLVideoView(frame: view.frame) #endif webRTCClient.startCaptureLocalVideo(renderer: localRenderer) webRTCClient.renderRemoteVideo(to: remoteRenderer) if let localVideoView = self.localVideoView { embedView(localRenderer, into: localVideoView) } embedView(remoteRenderer, into: view) view.sendSubviewToBack(remoteRenderer) }つまづきポイント
ユーザーは管理画面から作成ではなく、アプリのサインアップから作成してください。
管理画面から作成すると、パスワードの再設定をしていない状態になってしまい、別途コマンド操作でパスワードの再設定が必要になりますがAWS CLIから操作する場合に、シークレットキーを外す対応が必要です。
今回のサンプルの手順ではAWS CLIから対応するより、アプリのAPIから対応した方が良さそうです。
https://qiita.com/noobar/items/6615501b035e47792227issuesを投げたところ、デバッグでレスポンスの確認をしてくださいとの事で、確認しましたら、パスワードの再設定的なログがでたので、アプリ側から操作してパスワードを再設定したところ、アプリ認証が通りました。
https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios/issues/1挙動
モバイル
シグナリングチャンネルのConnect画面
プレビュー画面
AWS Kinesis Video Streams シグナリングチャンネル画面
Chromeなどで開いて下さい。
CloudWatch メトリクスなどで使用解析もできるので、価格プランなども検討できると思います。
複数端末での挙動
複数端末でのAWSRTCの配信技術 pic.twitter.com/1lIaz3U3WC
— DaisukeNagata (@dbank0208) December 12, 2019アプリケーションを動作させる上で、合計工数は4時間ぐらいで確認できました。
リアルタイム配信が身近なものになったと思いますが、カメラ機能などカスタマイズ性を向上させるのはRTC側を自作する必要があり、Safariだとメディア再生ビューワーが開かなかったので、自作してSafariを開かせる必要が出てきそうです。
関連資料 JSのWeb画面でビデオストリーミングWebRTCを利用する場合
今後も精進して、技術力を高めて、ゆくゆくは技術で還元していきます。
以上、貴重なお時間お読み下さいまして、誠にありがとうございます。
- 投稿日:2019-12-15T20:03:34+09:00
RxSwiftでUXを考慮した(Gmailアプリライクな)詳細画面を実装する
この記事は、Classi Advent Calendar 2019 の15日目の記事です。
こんにちは、ClassiのiOSアプリエンジニアの@yoko-yanです。
Classiは、サーバーサイド、特にRubyエンジニアが多くて、サーバーサイドの記事が多いと思うんですが、アプリも負けずに書いていきたいと思っているので、よろしくお願い致します。
最近は社内でRxSwift勉強会を開催するなど、社内のRxSwiftの啓蒙活動を行っています。そういったこともあり、せっかくなので、RxSwiftで試行錯誤して作った部分を公開できる範囲で記事にしてClassiのアプリも、しっかり作っているぞ的なイメージを持たせられればと思っています。
UX(ユーザーエクスペリエンス)を考慮した詳細画面とは
さて、現在開発に携わっているアプリの中では、いろいろな工夫を施しているんですが、その中でも、UXの体験向上として、工夫した箇所があるんですが、一覧画面と詳細画面という構成の中で、詳細の前後のページにアクセスしやすくしたいので、詳細画面で左右にスワイプすることで、次の詳細データや前のの詳細データを表示する体験があります。
これは、Gmailアプリが実装しているような、各メールの詳細ビューで左右にスワイプすることで、次の前のメールを表示する方法をイメージしてもらえれば、分かりやすいかと思います。
記事の内容について
前述したものを実現すべく、RxSwfitを使って、どんな風に実装したかを、記事にしたいと思います。
今回、実際に動かせるソースをGithubに公開できるとよかったんですが、時間の都合で用意できなかったので
別途、公開できるタイミングがあれば、公開したいなと思うので、今回は、ご了承ください。具体的な実装の解説
アプリのよくある実装として、一覧画面では、一覧データ取得のAPIを叩いて、あらかじめ決められた数(例えば20件など)を取得して一覧画面を作成していると思うので、Gmailのように一覧画面から詳細画面に遷移し、そこでスワイプで前後のデータに遷移できるように実装するには
1. スワイプで遷移した際のデータの切れ目で、ページングのAPIリクエストを実行
2. 追加で取得したデータを一覧ページとスワイプ画面で同期を取るという実装が必要になります。
これを、RxSwiftで実装してみます。
まず、スワイプでのページングを実装するにあたり、UICollectionViewだったり、UIPageViewControllerだったりで、いろいろ実装方法はあるかと思いますが、今回は、UIPageViewControllerで実装しました。
その理由は、すでに詳細画面をUIViewControllerとして実装していたので、それをそのまま使いたく、UIPageViewControllerなら、それを呼び出すだけでよかったのと、スワイプのいらない詳細画面は、直接、詳細画面のUIViewControllerを呼び出すことが出来るようにしたかったからです。概要
めちゃくちゃ雑ですが、ざっくり言うと、こんな感じで、ストリームを共有しています。
PageViewControllerについて
まず、PageViewControllerをどのように実装しているのかを見ていきます。
Item.swiftstruct Item: Decodable { let id: String let name: String }このアプリでは、上記のようなStructを扱うとして、以下のように実装しています。
PageViewController.swiftimport UIKit import RxSwift import RxCocoa class PageViewController: UIPageViewController { private let paginationRequestTrigger: PublishRelay<Void> private var items: [Item] = [] let disposeBag = DisposeBag() init(itemsStream: Observable<[Item]>, paginationRequestTrigger: PublishRelay<Void>, selectedItem: Any) { self.paginationRequestTrigger = paginationRequestTrigger super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) dataSource = self delegate = self let binder = Binder(self) { (vc, items: [Item]) in if vc.items.isEmpty { guard let index = items.firstIndex(where: { $0.id == selectedItem.id }) else { return } vc.items = items vc.setupPageViewController(initialIndex: index) } else if !items.isEmpty { vc.items = items } } itemsStream .filter { !$0.isEmpty } .bind(to: binder) .disposed(by: disposeBag) } required init?(coder aDecoder: NSCoder) { fatalError() } } private extension PageViewController { func setupPageViewController(initialIndex: Int) { setViewControllers([createDetailPageViewController(of: initialIndex) ?? UIViewController()], direction: .forward, animated: true, completion: nil) } func createDetailPageViewController(of index: Int) -> UIViewController? { guard items.indices.contains(index) else { return nil } // index out of range let item = items[index] let vc = DetailViewController() vc.item = item vc.view.tag = index return vc } } extension PageViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { let currentIndex = viewController.view.tag return createDetailPageViewController(of: currentIndex - 1) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { let currentIndex = viewController.view.tag return createDetailPageViewController(of: currentIndex + 1) } } extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { guard let nextIndex = pendingViewControllers.first?.view.tag else { return } guard items.count - 1 == nextIndex else { return } paginationRequestTrigger.accept(()) } }このPageViewControllerは、一覧画面で、セル(アイテム)をクリックした際に、インスタンス化されて呼び出されます。
このPageViewControllerでやってるRxSwiftを活用した実装のポイントとしては
- (1) 一覧取得APIを実行しているストリームを購読し、ListViewController(一覧画面)とデータを同期する
- (2) スワイプ時に表示するデータがない場合、ストリームを実行し、追加取得する
の2つです。
(1) 一覧取得APIを実行しているストリームを購読し、ListViewController(一覧画面)とデータを同期する
インスタンス生成時に一覧画面から、一覧取得APIを実行しているストリームと、そのトリガーと、選択されたデータを渡しています。
init(itemsStream: Observable<[Item]>, paginationRequestTrigger: PublishRelay<Void>, selectedItem: Any) {
引数 説明 itemsStream 一覧取得APIを実行し、 itemの配列が流れるストリーム paginationRequestTrigger itemsStreamを現在のページ指定を元に追加データを取得するトリガー selectedItem 一覧画面で選択したセルのデータ 以下の箇所で、ストリームに値が流れてきた際に、必要に応じてDetaiViewControllerを再セットします。
let binder = Binder(self) { (vc, items: [Item]) in if vc.items.isEmpty { guard let index = items.firstIndex(where: { $0.id == selectedItem.id }) else { return } vc.items = items vc.setupPageViewController(initialIndex: index) } else if !items.isEmpty { vc.items = items } } itemsStream .filter { !$0.isEmpty } .bind(to: binder) .disposed(by: disposeBag)(2) スワイプ時に表示するデータがない場合、ストリームを実行し、追加取得する
スワイプで次々にデータを表示する際、DetailViewControllerを生成して表示していますが、元になるデータは、インスタンス生成時に、initで渡されたデータなので、スワイプを続けているとデータの切れ目に辿りつきます。
そのタイミングで、一覧取得APIを実行して、新たにデータを追加取得したいのですが、ここで取得したデータは、一覧でも反映して(同期を取りたい)、一覧画面に戻った時に、同じストリーム実行せずに済ませたいので、initで渡されたトリガーを元に、一覧画面側のストリームを実行し、 (1)の箇所で、結果を反映させます。extension PageViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { guard let nextIndex = pendingViewControllers.first?.view.tag else { return } guard items.count - 1 == nextIndex else { return } paginationRequestTrigger.accept(()) } }このように実装することで、一覧画面と同じストリームや同じパラメータセットのAPIを、わざわざ実行せずに済むようにしました。
ListViewControllerについて
一覧画面を表示するListViewControllerについて、実装を見ていきます。
[補足]
余談ですが、このアプリの実装は、MVVM+RxSwiftで実装しているので、この記事でも、それで実装しているコードで記述します。
また、MVVM+RxSwiftで実装するにあたっては、以下の書籍を参考にさせて頂いています。
RxSwift研究読本3 ViewModel設計パターン入門
この書籍の中のsergdort氏のViewModelパターンを採用しています。ListViewControllerは、以下のように実装しています。
ListViewController.swiftfinal class ListViewController: UIViewController { private let disposeBag = DisposeBag() private let viewModel = ListViewModel() private let dataSource = DataSource() @IBOutlet private weak var tableView: UITableView! private let reloadRequestTrigger = PublishRelay<Void>() private let paginationRequestTrigger = PublishRelay<Void>() private let changeDataTrigger = PublishRelay<Item>() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() bind() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } // MARK: - Private private func bind() { let input = ListViewModel.Input( viewWillAppearStream: rx.sentMessage(#selector(viewWillAppear(_:))).map { _ in }, viewDidReachBottom: tableView.rx.reachedBottom.asObservable(), reloadRequestTrigger: reloadRequestTrigger.asObservable(), paginationRequestTrigger: paginationRequestTrigger.asObservable(), changeDataTrigger: changeDataTrigger.asObservable() ) let output = viewModel.transform(input: input) output.itemsStream .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) do { let binder = Binder(self) { (vc, error: Error) in // error } output.error .bind(to: binder) .disposed(by: disposeBag) } do { let binder = Binder(self) { (vc, item: Item) in let detailVC = PageViewController(itemsStream: output.itemsStream, paginationRequestTrigger: vc.paginationRequestTrigger, selectedItem: item) vc.navigationController?.pushViewController(detailVC, animated: true) } tableView.rx .modelSelected(Item.self) .bind(to: binder) .disposed(by: disposeBag) } output.loading .bind(onNext: { $0 ? LoadingIndicator.show() : LoadingIndicator.dismiss() }) .disposed(by: disposeBag) } } private class DataSource: NSObject, UITableViewDataSource { typealias Element = [Item] private var items: [Item] = [] func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let item = items[indexPath.row] let cell = UITableViewCell(style: .default, reuseIdentifier: "cell") cell.textLabel?.text = item.name return cell } } extension DataSource: RxTableViewDataSourceType { func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<Element>) { Binder(self) { dataSource, elements in dataSource.items = elements tableView.reloadData() } .on(observedEvent) } } extension DataSource: SectionedViewDataSourceType { func model(at indexPath: IndexPath) throws -> Any { return items[indexPath.row] } }ViewModelの処理の概要は、後述します。
このListViewControllerでやってることの概要としては
- (1) UIイベントに依存せず、任意で発火させたいトリガーを宣言する
- (2) 一覧取得APIを実行の責務は、ViewModelに持たせるため、各トリガーをViewModelへ渡す
- (3) ViewModelで流れたストリームの結果を処理
- (4) tableViewの各イベントをストリームで処理
(1) UIイベントに依存せず、任意で発火させたいトリガーを宣言する
private let reloadRequestTrigger = PublishRelay<Void>() private let paginationRequestTrigger = PublishRelay<Void>() private let changeDataTrigger = PublishRelay<Item>()ボタンクリックなど、UIイベントをトリガーとする場合は、RxCocoaで、イベントをObservable化すればいいですが、そうでなく任意に発火したい場合は、トリガー用のObservableを用意します。
各トリガーの説明は以下になります。
変数 説明 発火場所 reloadRequestTrigger 現在のページ番号を考慮しない初回のAPIリクエストを実行するトリガー Pull to Refresh やエラー時のリトライなど paginationRequestTrigger 現在のページ指定を元に追加データを取得するトリガー スクロールイベントなどに起因せず、任意に追加読み込みをしたい箇所 changeDataTrigger 特定のデータ (Item)だけを入れ替えるトリガー 編集画面から戻ってきた際に、該当するデータだけ反映したい場合など (2) 一覧取得APIを実行の責務は、ViewModelに持たせるため、各トリガーをViewModelへ渡す
各トリガーごとに、異なった処理が必要ですが、それぞのロジックは、ViewController側ではなくViewModelで処理を行います。
これは、ViewModelをテスタブルにして、ViewController側は何も気にせずに処理を実行させたいという意図があります。
現在、アドバイザーとしていろいろ教えて頂いているスーパーなiOSエンジニアの方が、『ViewControllerはアホな方がいい』と言ってました。
この格言、めっちゃ気に入りましたwViewModelへバインドしている箇所です。
let input = ListViewModel.Input( viewWillAppearStream: rx.sentMessage(#selector(viewWillAppear(_:))).map { _ in }, viewDidReachBottom: tableView.rx.reachedBottom.asObservable(), reloadRequestTrigger: reloadRequestTrigger.asObservable(), paginationRequestTrigger: paginationRequestTrigger.asObservable(), changeDataTrigger: changeDataTrigger.asObservable() )ここでのviewWillAppearStreamについては、その名前の通り、UIViewControllerのviewWillAppearに反応してフックしているトリガーです。
ここでのreachedBottomについては、UIScrollViewを拡張して、RxCocoaでスクロールのボトムを検知しているものです。(3) ViewModelで流れたストリームの結果を処理
ViewModelで組んだストリームの結果を、Viewに反映するためのロジックです。
let output = viewModel.transform(input: input) output.itemsStream .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) do { let binder = Binder(self) { (vc, error: Error) in // error } output.error .bind(to: binder) .disposed(by: disposeBag) }リクエストが成功した際のストリームをtableViewのdataSourceへバインドしているのと
リクエストが失敗した際のストリームをエラー処理する為の、Binderへバインドしています。(4) tableViewの各イベントをストリームで処理
これについては、特に目新しいことをやっていることもないので、割愛させて頂きます。
ListViewModelについて
前述させて頂いたように、@yimajoさんの書籍を元に、sergdort氏のViewModelパターンを採用しています。
こちらに関しては、@yimajoさんの書籍を参照頂くか、以下のサイトを参照ください。
ViewModel in RxSwift world - Serg Dort - MediumこのViewModelの責務としては、ItemをAPI経由で取得し、ストリームとしてViewControllerへ返すだけです。
が!、今回の実装の肝となる部分なので、少々細かく説明します。ItemResponse.swiftstruct ItemResponse: Decodable { let items: Observable<[Item]> public let pages: Pages } public struct Pages: Codable { public let currentPage: Int public let nextPage: Int? }APIから返ってくるレスポンスが上記のようだと仮定して、ListViewModelの処理は以下のようになります。
ListViewModel.swiftimport Foundation import RxSwift import RxCocoa import APIKit struct ListViewModel { private let disposeBag = DisposeBag() struct Input { let viewWillAppearStream: Observable<Void> let viewDidReachBottom: Observable<Void> let reloadRequestTrigger: Observable<Void> let paginationRequestTrigger: Observable<Void> let changeItemTrigger: Observable<Item> } struct Output { let loading: Observable<Bool> let itemsStream: Observable<[Item]> let error: Observable<Error> } func transform(input: Input) -> Output { let session = Session.shared let nextPageNumber = PublishRelay<Int?>() let startRequestTrigger = input.viewWillAppearStream.take(1).map { 1 } // 1 is a first page number let itemsRequestTrigger = input.reloadRequestTrigger.map { 1 } // 1 is a first page number let paginationRequestTrigger = PublishRelay.merge( input.viewDidReachBottom.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance), input.paginationRequestTrigger.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) ) .withLatestFrom(nextPageNumber) .compactMap { $0 } let load = PublishRelay.merge(startRequestTrigger, paginationRequestTrigger, itemsRequestTrigger).share() let sequence = load .flatMapLatest { nextPage -> Observable<Event<ItemResponse>> in return session.rx .send(ItemsRequest(page: nextPage)) .asObservable() .materialize() } .share() let elements = sequence.compactMap { $0.event.element }.share() elements .map { $0.pages.nextPage } .bind(to: nextPageNumber) .disposed(by: disposeBag) let elementsWithPagination = elements .scan([]) { $1.pages.currentPage == 1 ? $1.items : $0 + $1.items } .startWith([]) .share(replay: 1) let elementsWithChanged = input.changeItemTrigger .withLatestFrom(elementsWithPagination) { ($0, $1) } .map { arg -> [Item] in let item = arg.0 var items = arg.1 if let index = items.firstIndex(where: {$0.id == item.id}) { items[index] = item } return items } let itemsStream = Observable .merge(elementsWithPagination, input.reloadRequestTrigger.map { [] }, elementsWithChanged) .share(replay: 1) let loading = PublishRelay.merge(load.map { _ in true }, sequence.map { _ in false }) return Output( loading: loading, itemsStream: itemsStream, error: sequence.compactMap { $0.event.error } ) } }このListViewModelでやってることの概要としては
- (1) 各トリガーに流すデータをページ番号で統一
- (2) 各トリガーをマージして、トリガーが発火したらストリームが実行されるようにする
- (3) APIを実行して、ページ番号を購読
- (4) APIの実行結果を、ストリームの中で保持している前回の結果にマージ
- (5) 部分的に変更があった場合、ストリームの中で保持している結果を元に差し替える
- (6) APIリクエストが必要なトリガーと、APIリクエストが不要となるトリガーをマージして、ストリームを構成する
(1) 各トリガーに流すデータをページ番号で統一
最終的に、ストリームに流れるデータは、ページ番号を起因にして、変化するので、発火する際に流す値は、ページ番号を長す
viewWillAppearやreloadRequest発火時のリクエストについては、ViewControllerからページ番号を渡す必要がないので、ViewModelの中でストリームを直接、書き換えます。let nextPageNumber = PublishRelay<Int?>() let startRequestTrigger = input.viewWillAppearStream.take(1).map { 1 } // 1 is a first page number let itemsRequestTrigger = input.reloadRequestTrigger.map { 1 } // 1 is a first page number追加読み込みによるAPIリクエストは、一覧をスクロールして最下部に到達した場合、任意に読み込み発生させたい(PageViewController内)の両方のパターンがあるので、その二つのトリガーをマージします。
let paginationRequestTrigger = PublishRelay.merge( input.viewDidReachBottom.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance), input.paginationRequestTrigger.asObservable() .throttle(.seconds(1), latest: false, scheduler: MainScheduler.instance) ) .withLatestFrom(nextPageNumber) .compactMap { $0 }連打防止の為、throttleで無駄なストリームが流れないように制御しています。
(2) 各トリガーをマージして、トリガーが発火したらストリームが実行されるようにする
ここでは、3パターンあるトリガーのうち、どれかが発火されたらストリームが流れるようにマージします。
let load = PublishRelay.merge(startRequestTrigger, paginationRequestTrigger, itemsRequestTrigger).share()(3) APIを実行して、ページ番号を購読
一覧取得のAPIリクエストを実行しています。
let sequence = load .flatMapLatest { nextPage -> Observable<Event<ItemResponse>> in return session.rx .send(ItemsRequest(page: nextPage)) .asObservable() .materialize() } .share()ここでは、ItemsRequestの中身については、触れません。
単に、APIKitをラッピングしているのみです。RxSwiftのmaterializeで、APIの成功のストリームと失敗のストリームに分けて、成功のストリームが流れたときに、ページ番号を購読しています。
let elements = sequence.compactMap { $0.event.element }.share() elements .map { $0.pages.nextPage } .bind(to: nextPageNumber) .disposed(by: disposeBag)(4) APIの実行結果を、ストリームの中で保持している前回の結果にマージ
(成功の)ストリームの中で、保持しているデータに前回の結果が含まれていれば、それに新しい結果を追加して、それをストリームに流すようにします。
let elementsWithPagination = elements .scan([]) { $1.pages.currentPage == 1 ? $1.items : $0 + $1.items } .startWith([]) .share(replay: 1)これは、APIの結果が、2ページ目、3ページ目で、リクエストごとに、前ページの結果を含んでいない場合のAPIの仕様の場合で、ストリームの外から前回の結果を差し込まずに、時系列で保持している前回の値を効率よく使う為のロジックです。
最初、ストリームの外から差し込んじゃえというようなロジックを書いてたんですが、スーパーエンジニアにダメ出しされました。
今考えると当たり前ですが。汗(5) 部分的に変更があった場合、ストリームの中で保持している結果を元に差し替える
これも (6) と同じく、既にストリームの中に流れている値 (elementsWithPagination) を元に、部分的にデータを差し替えるロジックです。
編集を行った際などに、APIリクエストを伴わなずに、差し替えたいなと思った場合、上流のストリームから流してしまうと、かなり複雑なストリームになってしまうので、API実行後のストリームに対し、差し替えを行っています。let elementsWithChanged = input.changeItemTrigger .withLatestFrom(elementsWithPagination) { ($0, $1) } .map { arg -> [Item] in let item = arg.0 var items = arg.1 if let index = items.firstIndex(where: {$0.id == item.id}) { items[index] = item } return items }(6) APIリクエストが必要なトリガーと不要となるトリガーをマージして、ストリームを構成する
(5) で説明したようにAPI実行後のストリームの値に変化をつけて、その結果をViewControllerへ流したいので、本流のストリームとマージして、両方をトリガーとして、流れるようにしています。
let itemsStream = Observable .merge(elementsWithPagination, input.reloadRequestTrigger.map { [] }, elementsWithChanged) .share(replay: 1)ViewController側でこういった処理を行おうとすると、かなり助長なコードになるのが予想できるので、途中で話した『ViewControllerはアホな方がいい』というのは、非常に納得できるのではないでしょうか。
(補足) ローディングの表示について
ここまで触れてこなかったのですが、ViewController側で、くるくるのローディング表示を行うに当たって、ストリームが流れているかどうかで判断させることで、ViewControllerは、そのBool値を追いかけるだけでよくなります。
let loading = PublishRelay.merge(load.map { _ in true }, sequence.map { _ in false })上流のトリガーが発火したタイミングでtrue、ストリームが完了したタイミングでfalseを流すことで、ローディング表示のオンオフが容易になります。
DetailViewControllerについて
今回の説明では、解説は不要なので割愛させて頂きます。
最終的なストリームのイメージ
最終的に組んだストリームのイメージは、こんな感じになります。
厳密には、ちょっと違うかもしれませんが、Itemsの取得に関するストリームは、全てListViewModelの中のストリームを通って、取得している感じです。まとめ
実は、RxSwiftやリアクティブプログラミングは、Classiに入社する前は、興味はあれど、触ったことがなく、入社してからキャッチアップさせてもらったのですが、さすが教育分野の会社だけあって、社内メンバーの学習意欲の高さや、学習欲に対する理解度は高いです。
オブジェクト指向プログラミングを長くやっていたので、リアクティブプログラミングに切り替えるのは、かなり苦労しましたが、今ではかなり理解度もあがり、オブジェクト指向プログラミングに戻って、助長なプログラムを書いてしまうのが、怖いくらいになりました。
ただ、まだまだリアクティブプログラミングの奥は深く、ただでさえ、正解がないプログラミングの世界で、さらに正解がぼやけてしまう感じで、例えて言うならば、RPGゲームで、どの武器を使ってボスと戦かおうかの選択肢の中に、魔法や属性が加わって、武器の選択だけでは、戦えないといった感じです。
むしろ、リアクティブプログラミングという方法を知ってしまった今は、バル○やパルプン○といった呪文を覚えてしまった感覚にもなり、全てを破壊したり、混乱に陥れる可能性もあるヤバイスキルを身につけてしまった感もあります。
ただ、Appleも、Combineフレームワークを発表したように、時代は、リアクティブプログラミングなど、時系列を考慮したプログラミングを組めるようにならないと、取り残されていくなと感じました。
今回の実装も、複雑な処理は、ほとんど、ViewModelの中にコンパクトに収められていて、RxSwiftを導入していなければ、色々煩雑なロジックを書く必要があったなと、RxSwiftを採用したメリットをかなり感じています。
ここまで来るには、いろんな壁にぶち当たって、その都度、社内のiOSエンジニアと、アドバイスを頂いているスーパーエンジニアのご協力もあって、実装できましたので、リスペクトを兼ねて、解説する記事を書くことにしました。
おかげで、自分自身もかなり、RxSwiftやリアクティブプログラミングの理解が深まり、こういった記事が書けるようになったので、本当に感謝しています。Classiには、こんなアプリエンジニアがいるんだというようなイメージアップに繋がればいいなと思っていますので、是非、今後ともよろしくお願いいたします。
- 投稿日:2019-12-15T13:10:28+09:00
iOS13.3 @Publishedでの値更新からsinkが呼ばれなくなった?(ミス解決)
iOS13.3では@Publishedでの値更新でsinkが呼ばれない?
起きたこと
iOS13.3に更新したところ、SwiftUIのEnvironmentObject上の@Publishedの更新通知が受け取れなくなりちょっとハマりました。結局の所、もともと更新通知を受け取る処理を書き間違えていたことが原因でした。
修正前
let _ = self.model.$text.receive(on: DispatchQueue.main) .sink { (value) in print("receive") self.text = value }修正後
var task:AnyCancellable? = nil ... self.task = self.model.$text.receive(on: DispatchQueue.main) .sink { (value) in print("receive") self.text = value }きちんと戻り値のAnyCancellableを保持していないことが問題でした。考えてみれば納得なのですが、iOS13.2までは動いていたので見落としていました。戒めの意味を込めて記録しておきます。
- 投稿日:2019-12-15T11:01:26+09:00
CAShapeLayer による図形のストロークアニメーション
iOS で図形を描画したい場合、
CAShapeLayer
を使うと、様々な図形を描画することができます。
また、Core Animation を使うことで、図形をアニメーションさせることができます。簡単な実例を Xcode の Playground で試してみます。
Playground の準備
まず、Xcode で iOS 用の Playground を作成して、View を表示します。
import UIKit import PlaygroundSupport let view = UIView() view.frame = CGRect(x: 0, y: 0, width: 400, height: 400) view.backgroundColor = .white PlaygroundPage.current.liveView = viewView のサイズはなんでも良いですが、とりあえず 400x400 にしておきました。次のように白い View が表示されます。
CAShapeLayer の作成
先ほどの View に
CAShapeLayer
を追加します。let shapeLayer = CAShapeLayer() shapeLayer.frame = CGRect(x: 0, y: 0, width: 400, height: 400) shapeLayer.lineWidth = 20.0 shapeLayer.strokeColor = UIColor.blue.cgColor shapeLayer.fillColor = UIColor.cyan.cgColor view.layer.addSublayer(shapeLayer)まだ図形を設定していませんが、ひとまず線の幅(
lineWidth
)、線の色(strokeColor
)、塗りつぶしの色(fillColor
)を設定しておきます。ここでは、単に何も表示しない
shapeLayer
を追加しただけなので、表示は変わりません。弧の描画
弧を描画してみます。
let center = CGPoint(x: 200, y: 200) let radius = CGFloat(100) let startAngle = CGFloat(0.125 * .pi) let endAngle = CGFloat(1.875 * .pi) let arc = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) shapeLayer.path = arc.cgPath
UIBezierPath
で、中心座標、半径、開始角、終了角を指定すれば、弧が作成できます。角度の数値については リファレンス に記載されています。3時の方向を0として、時計周りにラジアンで指定します(数学では反時計回りが一般的ですが、ここでは逆向きです)。ここでは、1/8 π ラジアンから、(2 - 1/8) π ラジアンまでの円弧を指定しました。
弧のストロークアニメーション
弧の描画をアニメーションで表示します。
let animation = CAKeyframeAnimation(keyPath: "strokeEnd") animation.values = [0.0, 1.0, 1.0] animation.keyTimes = [0.0, 0.3, 1.0] animation.duration = 3.0 animation.repeatCount = .greatestFiniteMagnitude shapeLayer.add(animation, forKey: nil)
CAShapeLayer
は様々なアニメーションが可能ですが、今回はstrokeEnd
を使います。
strokeStart
とstrokeEnd
はペアで使うプロパティで、CAShapeLayer
のpath
のうち、実際に描画する範囲を0.0
から1.0
の間で指定します。デフォルトではstrokeStart
が0.0
でstrokeEnd
が1.0
、つまりpath
の全体を描画します。仮にstrokeEnd
を0.5
にすると、path
の最初から半分の位置までが描画されます。
strokeEnd
は リファレンス にも書いてあるように Animatable なプロパティであり、値の変化をアニメーションで表示できます。そこで、strokeEnd
を0.0
から1.0
にアニメーションで変化させることで、線が徐々に描画されるアニメーションになります。また、アニメーションには
CABasicAnimation
がよく使われますが、今回はCAKeyframeAnimation
を使いました。
values
とkeyTimes
を使って値の変化を指定します。上記のコードは、時間0.0
から0.3
の間に値を0.0
から1.0
に変化させ、時間0.3
から1.0
の間では値は1.0
から1.0
のままにする、という指定をしています。アニメーション全体の時間duration
は 3 秒に指定しているため、時間0.3
は 3 x 0.3 = 0.9 秒を意味します。アニメーションを繰り返す場合に、いったん静止状態を挟むのは
CAKeyframeAnimation
が簡単かと思います。特に繰り返しが必要なければ、CABasicAnimation
を使うほうが素直で分かりやすいかと思います。弧のストロークアニメーション pic.twitter.com/Of29omEqaE
— 宇佐見 公輔 (@usamik26) December 15, 2019扇形の描画
複数の線を連結させて図形を描画してみます。
let center = CGPoint(x: 200, y: 200) let radius = CGFloat(100) let startAngle = CGFloat(0.125 * .pi) let endAngle = CGFloat(1.875 * .pi) let fanShape = UIBezierPath() fanShape.move(to: center) fanShape.addLine(to: CGPoint(x: center.x + radius * cos(startAngle), y: center.y + radius * sin(startAngle))) fanShape.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) fanShape.close() shapeLayer.path = fanShape.cgPathまず、円の中心にペンを置き(
move
)、円周に向かって線を引きます(addLine
)。ある点から距離radius
、角度 θ 方向の点の座標は、X 座標にradius * cos(θ)
を、Y 座標にradius * sin(θ)
を足せば求められます。次に、この線に続けて弧を書きます(
addArc
)。最後にclose
を呼ぶと、最初の点まで直線で繋がれて閉じた線ができあがります(なお、close
しないと閉じた線という扱いにならないため、始点と終点が繋がって描画されずに不自然になります)。扇形のストロークアニメーション
扇形の描画をアニメーションで表示するコードは、弧のときとまったく同じです。
let animation = CAKeyframeAnimation(keyPath: "strokeEnd") animation.values = [0.0, 1.0, 1.0] animation.keyTimes = [0.0, 0.3, 1.0] animation.duration = 3.0 animation.repeatCount = .greatestFiniteMagnitude shapeLayer.add(animation, forKey: nil)どんな図形でも
strokeEnd
を0.0
から1.0
に変化させれば良いので、難しいことを考える必要はありません。扇形のストロークアニメーション pic.twitter.com/z4HFOG3YNe
— 宇佐見 公輔 (@usamik26) December 15, 2019まとめ
Xcode Playground で今すぐ試せるアニメーションのコードを紹介しました。参考になれば幸いです。
- 投稿日:2019-12-15T08:04:58+09:00
Flutterウィークリー #85
Flutterウィークリーとは?
FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/この記事は#85の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-85※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。
読み物&チュートリアル
Flutterオンラインおよびオフライン接続を実装する方法
https://alltechsavvy.com/online-offline-connectivity-in-flutter/
Sagar Shendeによるこのチュートリアルで、ユーザーがオンラインまたはオフラインのときにアプリ内の情報を処理する方法を学びます。
Flutter Webを使用してサーバーにファイルをアップロードする
https://rodolfohernan20.blogspot.com/2019/12/upload-files-to-server-with-flutter-web.html
Flutter Webアプリでファイルのアップロードを処理できるようにするための正確な手順。
Flutterサポートする真新しいDart Pad.dev
https://medium.com/dartlang/a-brand-new-dartpad-dev-with-flutter-support-16fe6027784
Dart PadはオンラインでFlutterサポートするようになりました!
EyeEm Flutter ed
https://medium.com/flutter-community/eyeem-fluttered-3ad12efc8b16
EyeEmのŁukaszWiśniewskiは、既存のAndroidおよびiOSネイティブアプリにFlutterを追加した後の調査結果について説明しています。
暗黙的なアニメーションによるFlutterアニメーションの基本
https://medium.com/flutter/flutter-animation-basics-with-implicit-animations-95db481c5916
Alethea K. Flowersがアニメーションの基本を紹介します。ビデオも利用できます。
Flutter RaisedButtonクックブック
https://medium.com/flutter-community/flutter-raisedbutton-cookbook-7c3d4a82b26f
RaisedButtonについて知りたいことはすべて、Aneesh Joseによって説明されました。
Flutter Firebaseダイナミックリンクの処理
https://medium.com/flutter-community/handling-firebase-dynamic-links-in-flutter-7c1de6a4e2e
Nikita Gandhiによるこの記事のおかげで、 FlutterでFirebase Dynamicリンクを最大限に活用できます。
Flutterパッケージにアセットを含める
https://medium.com/flutter-community/including-assets-in-a-flutter-package-dd4a82a38ca9
Flutterパッケージでアセットを適切に処理します。
Flutter Developerになるためのロードマップ(初心者向けリソース)
Flutterを始めた人のために、Ashish RawatはFlutterを学ぶための膨大なリソースのリストをFlutterます。
ビデオ&メディア
始まりFlutterウィジェットツリーを理解します
https://www.youtube.com/watch?v=Xu92WAlf0vI&feature=share
最適化の側面を含むウィジェットの基本と、それらを再利用するさまざまな方法の理解。
AnimatedBuilderとAnimatedWidgetを使用してカスタムの明示的なアニメーションを作成するFlutter in Focus
https://www.youtube.com/watch?v=fneC7t4R_B0&t=174s
このFlutter in Focusのエピソードでは、エミリー・フォーチュナが、AnimatedBuilderまたはAnimatedWidgetを他のアニメーションウィジェットと比較して使用する理由を紹介しています。
Flutterクロックコンテスト-プロトタイプクロックの構築-ライブストリーム
https://www.youtube.com/watch?v=HYrbFTZ88nY
Flutter Clockコンテストに参加する場合、このビデオは完全なクロック作成プロセスのライブストリームです。
REST APIとの対話
https://www.youtube.com/watch?v=ZMNp9Ev6cl0
httpパッケージを使用してDart Flutter REST APIにGETリクエストを行う方法を学びます。
スーパー列挙? Dart & Flutterチュートリアル-カスタムデータストア
https://www.youtube.com/watch?v=iS_05wRScic
super_enumパッケージを使用してDartで列挙型を使用する方法。
トグルボタン(今週のFlutterウィジェット)
https://www.youtube.com/watch?v=kVEguaQWGAY&list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG&index=64
ユーザーに関連する多数のオプションから選択を許可しますか? ToggleButtonsをアプリに追加すると役立ちます。
ライブラリ&コード
GitHub-apgapg / flutter_physics_concepts
https://github.com/apgapg/flutter_physics_concepts
視覚物理学学習アプリのソースコード。
GitHub-flutterigniter / flutter_data_stream_builder
https://github.com/flutterigniter/flutter_data_stream_builder
実用的なデフォルトを備えた実用的なStreamBuilder
GitHub-JonathanMonga / flutter_trianglify
https://github.com/JonathanMonga/flutter_trianglify
AndroidおよびiOS向けの美しい三角形アートビューを生成するFlutterライブラリ。
GitHub-Milad-Akarie / auto_route_library
https://github.com/Milad-Akarie/auto_route_library
AutoRouteは、ナビゲーションに必要なすべてのものが自動的に生成されるルート生成ライブラリです。
GitHub-rodydavis / easy_google_maps
https://github.com/rodydavis/easy_google_maps
WebおよびモバイルでのFlutter用の簡単なGoogleマップ
GitHub-yako-dev / flutter-settings-ui
https://github.com/yako-dev/flutter-settings-ui
Flutterアプリのネイティブ設定画面を数分で作成します。
GitHub-yako-dev / flutter-status-alert
https://github.com/yako-dev/flutter-status-alert
Appleシステムのような自己非表示ステータスアラートを表示します。ユーザーフローを中断することなくユーザーに通知するのに適しています。
- 投稿日:2019-12-15T08:03:31+09:00
Flutterウィークリー #84
Flutterウィークリーとは?
FlutterファンによるFlutterファンのためのニュースレター
https://flutterweekly.net/この記事は#84の日本語訳です
https://mailchi.mp/flutterweekly/flutter-weekly-84※Google翻訳を使って自動翻訳を行っています。翻訳に問題がある箇所を発見しましたら編集リクエストを送っていただければ幸いです。
読み物&チュートリアル
FlutterモジュールをネイティブのAndroidプロジェクトに追加し、Codemagicでテストする方法
https://blog.codemagic.io/flutter-module-android-yaml/
Flutterモジュールを既存のAndroidアプリに統合する方法に関するSouvik Biswaによるチュートリアル。
Flutterアプリケーションで複数のテーマを管理する
https://blog.dammak.dev/managing-multiple-themes-in-flutter-application-ck3396mer01542as1rxv7xx5r
Damola Adekoyaは、アプリケーションにさまざまなテーマを実装する方法を示します。
Flutter BLoCの使用開始
https://dev.to/netguru/getting-started-with-flutter-bloc-1pkm
Kacper Kogutによるこの記事でBLoCを適切に使用する方法を学ぶ
FlutterアプリでGoogleマップにカスタムマーカーを作成する
https://infinum.com/the-capsized-eight/creating-custom-markers-on-google-maps-in-flutter-apps
JosipKrnjićが、 Flutter Googleマップにカスタムメイドマークを追加する方法について詳しく説明します。
電子ブック-簡潔にFlutter
https://www.syncfusion.com/ebooks/flutter-succinctly
Flutter Ed Freitasによる無料の電子ブック
Flutterアプリの広告
https://medium.com/@greg.perry/ads-in-your-flutter-app-16ad82ce698a
Greg Perryによるこの記事で、アプリに広告を挿入してプロジェクトを収益化する方法を学びましょう
Flutter Cameraプラグインの探索
https://medium.com/@divyanshub024/exploring-flutter-camera-plugin-d2c54ac95f05
Divyanshu BhargavaはFlutterのプラグインカメラを詳細に分析します。
Flutterアラートダイアログからカスタムダイアログ
https://medium.com/flutterpub/flutter-alert-dialog-to-custom-dialog-966195157da8
Ishan Fernandoによるこの記事のおかげで、 Flutterでダイアログを作成する方法を学びます。
Flutter for Web:ポートフォリオWebサイトの構築
https://medium.com/flutter-community/flutter-for-web-building-a-portfolio-website-3e9865710efe
Flutterを使用してポートフォリオWebサイトを作成しますか? Aditya Gurjarは、必要なすべてのステップを示します。
Flutter Web + Netlify:2分で適切な方法で継続的に展開
私はNetlifyの大ファンであり、Netlifyを頻繁に使用しています。 Shahrukh Siddiquiが、NetlifyでFlutter Webサイトを簡単に展開する方法についての記事を書きました。
Flutter Webのレイアウトテンプレートと基本ナビゲーション
Dane Mackierは、Webプロジェクトのテンプレートを紹介します。
モジュラーFlutterアプリ—設計と考慮事項
https://medium.com/flutter-community/modular-flutter-apps-design-and-considerations-59c5ac65352
アプリのモジュール化の長所と短所、およびGonçaloPalmaによるAndroidのモジュール化との違いに関するレビュー。
ファイル名を指定して実行Flutter GitHubのアクションのドライバーテスト- Flutterコミュニティ
https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab
Katarina Sheremetは、Githubアクションを使用してFlutterテストを実行する方法を説明します。
あなたの声を聞く— 2019年第3四半期のFlutterユーザー調査からの教訓
https://medium.com/flutter/we-hear-you-learnings-from-q3-2019-flutter-user-survey-af588dbd71b1
別の四半期がFlutterすると、別のFlutter調査結果がFlutterチームによって共有されます。
FlutterでAPIを操作する
https://medium.com/flutter-community/working-with-apis-in-flutter-8745968103e9
FlutterからFlutterへのアクセスに問題がありますか? Pooja Bhaumikは、必要なすべての部品について説明します。
ビデオ&メディア
Flutter Frameworkを使用してモバイルプログラマになるための22の短いレッスン
https://dev.to/zaiste/22-short-lessons-to-become-a-mobile-programmer-using-flutter-framework-d9j
Flutterさまざまな側面を示すためにザイステによって作成された膨大なビデオのリスト。
ColorFiltered( Flutter Widget of the Week)
https://www.youtube.com/watch?v=F7Cll22Dno8&list=PLjxrf2q8roU23XGwz3Km7sQZFTdB996iG&index=62
ColorFilteredウィジェットをチェックしてください。これにより、奇妙で素晴らしい方法で色を変更およびモーフィングできます。
Flutter Generated Dependency Injection – Kiwiチュートリアル
https://www.youtube.com/watch?v=1CHicTngyZY&feature=youtu.be
このチュートリアルでは、コード生成のパワーとkiwiパッケージを活用して、すぐに依存関係注入コンテナーを構成する方法を学習します。
最新バージョンのflutterとdartをraspberry pi 4にインストールし、アプリケーションを作成する
https://www.youtube.com/watch?v=SHc3NB1LdlI&feature=youtu.be
ラズベリーパイにフラッターをインストールし、フラッターアプリを作成する方法に関する4分間のチュートリアル。
Flutterバイナリクロックを作成してバイナリを学ぶ
https://www.youtube.com/watch?v=VkTj1U_exwA&feature=youtu.be
バイナリの仕組みを知っていますか? Flutterバイナリクロックを構築して、基本を学びます。 2020年1月22日の競技期限前に独自の時計を作成します。
ナビゲータ付きリストビュー| Flutterチュートリアル
https://www.youtube.com/watch?v=OpMyn7SdAWo
2番目の画面に移動して、 FlutterでListViewを作成する方法を学びましょう!
組み込みの明示的なアニメーションを使用して最初の方向アニメーションを作成する
Flutter in Focusのこのエピソードでは、Andrew Fitz GibbonがFlutter明示的なアニメーションを紹介しています。
TweenAnimationBuilderを使用して独自のカスタム暗黙アニメーションを作成する
https://www.youtube.com/watch?v=6KiPEqzJIKQ&list=PLjxrf2q8roU2HdJQDjJzOeO6J3FoFLWr2&index=20
このエピソードではFlutterフォーカスで、エミリー・フォーチュナショーはどのようにTweenAnimationBuilderを使用するFlutterアプリの基本的なアニメーションを構築します。
FlutterとFCMを使用したスマートプッシュ通知
https://www.youtube.com/watch?v=2TSm2YGBT1s&feature=youtu.be
Firebase Cloud Messaging(FCM)を使用してFlutterプッシュ通知を単一のデバイス、トピック、またはユーザーセグメントに送信します。
パフォーマンスの問題についてFlutterアプリケーションのプロファイルを作成するツールとテクニック
https://www.youtube.com/watch?v=GL61CotxCmM
このビデオでは、コードの潜在的なパフォーマンスの問題を発見するために、 Flutterアプリケーションのプロファイルを作成するためのいくつかのツールと手法について説明します。
ライブラリ&コード
GitHub-dreamsoftin / flutter_wordpress
https://github.com/dreamsoftin/flutter_wordpress
Flutter WordPress API
Flutterガイド
https://github.com/devonfw-forge/devonfw4flutter
Flutter基本とクリーンで構造化されたFlutter開発のギャップを埋めることを目的としたガイド。
faob-dev / flutter_circular_text
https://github.com/faob-dev/flutter_circular_text
Flutter円形テキストウィジェット
GitHub-ganeshsp1 / Flutter Bluetooth_PCControl
https://github.com/ganeshsp1/Flutter_Bluetooth_PC_Control
Bluetoothを使用してPCを制御するFlutterアプリ
GitHub-モジュロ値/ flutter_audio_wav_demo
https://github.com/modulovalue/flutter_audio_wav_demo
Dart .wavファイルを作成し、 Flutter視覚化します。
GitHub-rxlabz / boat_heroes
https://github.com/rxlabz/boat_heroes
Flutterヒーロートランジションの例
- 投稿日:2019-12-15T06:43:04+09:00
Create MLとCore MLを使って、カメラに写った人が嵐のメンバーの誰かをリアルタイムで判定する
この記事は iOS#2 Advent Calendar 2019 15日目の記事です。
初めて Advent Calendar に参加するので若干の緊張があります笑
よろしくお願いします!※Qiita界隈に愛嬌を振りまいていくスタンス今回は、「Create MLとCore MLを使って、カメラに写った人が嵐のメンバーの誰かをリアルタイムで判定する」という題でやっていきます。Core MLに興味ある方は、私の他にも @cthxn77r さんや @takashico さんが今年のアドベントカレンダーに投稿されていましたので、そちらもどうぞ!※Qiita界隈に"全力で"愛嬌を振りまいていくスタンス
やったこと
以下の動画のようなことができるようになります。
何番煎じだよと思いましたが、Core ML使ってみるならこういうことしたいなーと考えていたので、それが実現できて嬉しかったです。題材に嵐を選んだのは、人数が少なくて正解しやすそうだなと思ったからです。以下に、これを実装した手順を書いていきます。
教師データの作成
API を叩かずに Google から画像収集をする
を使用させていただきました。メンバー1人あたり500枚ほど集めました。そこから顔部分を切り出してデータを集めるのですが、
PyTorchを使って日向坂46の顔分類をしよう!
を参考にさせていただきました。今回はモデルの作成にCreate MLを使おうと思っていたので、画像のサイズは300*300を指定しております。※画像分類モデルの作成
あと、どこかで教師データにはノイズが入っていたほうが良いと聞いたことがあったので、「他人」というラベルの画像も同じくらい用意しました。
モデルの作成
XcodeのOpen Developer Toolをクリックすると、Create MLがあります。そこをクリックし、Create MLのアプリを起動します。
メニューの File > New Project を選択します。今回は画像から正しいラベルをつけるモデルを作るため、 Image Classifierを選択しましょう。
そしてプロジェクト名を決めれば、教師データとテストデータを選択する画面になります。あとは教師データとテストデータのフォルダを選択し、Trainボタンを押すだけで簡単にモデルが作れてしまいます。このとき、教師データのフォルダ内にある画像フォルダの名前がそのままラベルになるので、テストフォルダの名前と一致するように気をつけてください。
このモデルを使いたかったら、生成されたモデルをドラッグ&ドロップでXcodeのプロジェクト内に引っ張ってくるだけ!簡単ですね(Uber EatsのCM風に)
サンプルアプリの実装
以下に、サンプルアプリのプログラムを貼っておきます。コードは
Keras + iOS11 CoreML + Vision Framework による、ももクロ顔識別アプリの開発
Swiftでアイマスの画像認識やってみる
を大変参考にさせていただきました。ありがとうございました。
カメラを使用するので、info.plistでカメラの使用するための確認文言を入れておいてください。
ViewController.swiftimport UIKit import AVFoundation import Vision import CoreML class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { @IBOutlet weak var cameraView: UIView! @IBOutlet weak var faceImageView: UIImageView! @IBOutlet weak var resultLabel: UILabel! var ciImage: CIImage? var captureLayer: AVCaptureVideoPreviewLayer? override func viewDidLoad() { super.viewDidLoad() setupCamera() } override func viewDidLayoutSubviews() { captureLayer?.frame = cameraView.bounds } func setupCamera() { let session = AVCaptureSession() captureLayer = AVCaptureVideoPreviewLayer(session: session) cameraView.layer.addSublayer(captureLayer!) guard let device = AVCaptureDevice.default(for: .video) else { return } guard let input = try? AVCaptureDeviceInput(device: device) else { return } let output = AVCaptureVideoDataOutput() output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera")) session.addInput(input) session.addOutput(output) session.startRunning() } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { if connection.videoOrientation != .portrait { connection.videoOrientation = .portrait return } guard let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } ciImage = CIImage(cvImageBuffer: buffer) faceDetection(buffer) } func faceDetection(_ buffer: CVImageBuffer) { let request = VNDetectFaceRectanglesRequest { (request, error) in guard let results = request.results as? [VNFaceObservation] else { return } if let image = self.ciImage, let result = results.first { let face = self.getFaceCGImage(image: image, face: result) if let cg = face { self.showPreview(cgImage: cg) self.scanImage(cgImage: cg) } } } let handler = VNImageRequestHandler(cvPixelBuffer: buffer, options: [:]) try? handler.perform([request]) } func getFaceCGImage(image: CIImage, face: VNFaceObservation) -> CGImage? { let imageSize = image.extent.size let box = face.boundingBox.scaledForCropping(to: imageSize) guard image.extent.contains(box) else { return nil } let size = CGFloat(300.0) let transform = CGAffineTransform( scaleX: size / box.size.width, y: size / box.size.height ) let faceImage = image.cropped(to: box).transformed(by: transform) let ctx = CIContext() guard let cgImage = ctx.createCGImage(faceImage, from: faceImage.extent) else { assertionFailure() return nil } return cgImage } private func showPreview(cgImage: CGImage) { let uiImage = UIImage(cgImage: cgImage) DispatchQueue.main.async { self.faceImageView.image = uiImage } } func scanImage(cgImage: CGImage) { let image = CIImage(cgImage: cgImage) guard let model = try? VNCoreMLModel(for: ArashiClassifier().model) else { return } let request = VNCoreMLRequest(model: model) { request, error in guard let results = request.results as? [VNClassificationObservation] else { return } guard let mostConfidentResult = results.first else { return } DispatchQueue.main.async { self.resultLabel.text = mostConfidentResult.identifier } } let requestHandler = VNImageRequestHandler(ciImage: image, options: [:]) try? requestHandler.perform([request]) } } extension CGRect { func scaledForCropping(to size: CGSize) -> CGRect { return CGRect( x: self.origin.x * size.width, y: self.origin.y * size.height, width: (self.size.width * size.width), height: (self.size.height * size.height) ) } }注意した部分は、ただカメラの撮影した画像をモデルに読み込ませるのではなく、Vision.Frameworkで顔部分を探索してから判定を行うようにしたことですね。CIImage型のプロパティをつくりカメラで読み込んだ画像を一時保存しておくことで実現しています。
結果
実際に使っているところは上に貼った動画をご覧ください。
正直、使い物になるとは言い難い…!!ぶっちゃけ、Create MLで作ってる最中でテストの正解率が50%でした。
個人的な感覚ですが、実用的になるには80〜90%まではいかないとと思っているので、もっとデータを集めるなり、Create MLがもっといろいろなパラメータをいじれるようになるのを待つ必要はあるなと思いました。
ちなみに、私の顔写真をカメラで写してみると見事に「他人」と判定されました。
私の顔がジャニーズ判定されないのはおかしいのでもっと改良の余地がありますね。感想
アプリに機械学習を取り込むこと自体はすごい簡単にできて、その容易さに驚きました。そして、冷静に考えるとメンバー5人(プラス他人)の選択肢がある中で、5割の正答率を誇るのは割とすごい気がしました。(Pythonで作られた他モデルの精度には目をつぶりながら)
さらにこの後やったのですが、撮影した動画の1枚1枚ごとの結果を保存して比較的頻繁に出てくる名前を出力するようにすると、正答率は上がった気がします(体感ですが)。このような、モデルに通す前にあらかじめ顔部分を調べるとか、一瞬の結果を保存して一番多い答えを出すことで正答率を上げるとか、そこらへんがアプリエンジニアが機械学習と向き合っていく際に使うノウハウなのかな、って気がしました。
明日は @S_Shimotori さんで、ダークモードについてのお話みたいですね。
私が働いている会社のアプリが今ダークモードに対応するか否かの真っ只中なので、楽しみです!※Qiita界隈に(ry以上、ありがとうございましたー!
- 投稿日:2019-12-15T02:11:43+09:00
複数クラスへ同時にデリゲート通知を送る方法
デリゲートによる通知を送る際は通常1個のクラスしか通知先に指定できませんが、複数のクラスへ同時に通知を送りたい場合があるかと思います。
同時に通知を送るにはNotificationCenterを使う方法もありますが、この手法は通知元から受け取ったデータを通知先で判別するのが困難です。一方、デリゲートは通知先へ渡すデータを直接引数に指定することができます。
そこでデリゲートによって複数のクラスへ同時に通知を送る方法を考えてみました。初めての投稿で、まだ言語の知識も浅いので大目に見ていただけると幸いです。
デリゲートモデルの定義
DelegateCenter
通知先クラスのリストを持ち、実際に通知を送るクラスです。
DelegateCenter.h#import "DelegateObject.h" @interface DelegateCenter : NSObject @property (nonatomic) id delegate; - (void)doDelegate; @endDelegateCenter.m#import "DelegateCenter.h" @implementation DelegateCenter #pragma mark - Setter - (void)setDelegate:(id)delegate { // 一般的なデリゲートと同じように通知先を指定できるように、セッタを書き換える DelegateObject *delegateObject = [DelegateObject new]; delegateObject.delegate = delegate; if (self.delegate) { [self.delegate addObject:delegateObject]; } else { NSMutableArray <DelegateObject *> *mDelegate = [@[delegateObject] mutableCopy]; _delegate = mDelegate; } } #pragma mark - Delegate Trigger - (void)doDelegate { if (self.delegate) { for (DelegateObject *delegateObject in self.delegate) { if ([delegateObject.delegate respondsToSelector:@selector(delegateCenter:)]) { [delegateObject.delegate delegateCenter:self]; } } } } @endDelegateObject
デリゲートプロトコルを定義し、個々の通知先クラスを保持するクラスです。
メモリリークを避けるためにweakプロパティで保持しています。DelegateObject.h@class DelegateCenter; @protocol Delegate <NSObject> - (void)delegateCenter:(DelegateCenter *)delegateCenter; @end @interface DelegateObject : NSObject @property (weak, nonatomic) id <Delegate> delegate; @endデリゲートモデルの使用例
3個のViewControllerをそれぞれデリゲートの通知先に指定し、そのいずれかのクラスから自身を含む全クラスへ通知を送ります。
FirstViewController.m@interface FirstViewController () <Delegate> @property (nonatomic) DelegateCenter *delegateCenter; @end @implementation FirstViewController - (void)viewDidLoad { [super viewDidLoad]; // デリゲート管理クラスの通知先に自身を登録 self.delegateCenter = [DelegateCenter new]; self.delegateCenter.delegate = self; } - (IBAction)tapNext:(UIButton *)sender { // ボタン押下で次のVCへ遷移 SecondViewController *vc = [SecondViewController new]; // デリゲート管理クラスを共有する vc.delegateCenter = self.delegateCenter; [self presentViewController:vc animated:YES completion:nil]; } - (void)delegateCenter:(DelegateCenter *)delegateCenter { // 通知を受け取ると呼ばれる } - (IBAction)tapDelegate:(UIButton *)sender { // 通知先に登録されている全クラスへ通知を送る [self.delegateCenter doDelegate]; } @endSecondViewController.h@interface SecondViewController : UIViewController @property (nonatomic) DelegateCenter *delegateCenter; @endSecondViewController.m- (void)viewDidLoad { [super viewDidLoad]; // デリゲート管理クラスの通知先に自身を追加 self.delegateCenter.delegate = self; } - (IBAction)tapNext:(UIButton *)sender { // ボタン押下で次のVCへ遷移 ThirdViewController *vc = [ThirdViewController new]; // デリゲート管理クラスを共有する vc.delegateCenter = self.delegateCenter; [self presentViewController:vc animated:YES completion:nil]; } - (void)delegateCenter:(DelegateCenter *)delegateCenter { // 通知を受け取ると呼ばれる } - (IBAction)tapDelegate:(UIButton *)sender { // 通知先に登録されている全クラスへ通知を送る [self.delegateCenter doDelegate]; } @endThirdViewController.h@interface ThirdViewController : UIViewController @property (nonatomic) DelegateCenter *delegateCenter; @endThirdViewController.m@implementation ThirdViewController - (void)viewDidLoad { [super viewDidLoad]; // デリゲート管理クラスの通知先に自身を追加 self.delegateCenter.delegate = self; } - (void)delegateCenter:(DelegateCenter *)delegateCenter { // 通知を受け取ると呼ばれる } - (IBAction)tapDelegate:(UIButton *)sender { // 通知先に登録されている全クラスへ通知を送る [self.delegateCenter doDelegate]; } @endこれで、各クラスのdelegateボタンを押下すると自身を含む複数の通知先に通知を送れるようになります。
- 投稿日:2019-12-15T01:27:29+09:00
パラメタライズドテストのメリット解説(Swift)
はじめに
本記事は Swift Advent Calendar 2019 の14日目の記事です。
パラメタライズドテストのメリットについて解説します。注意
本記事は 「YUMEMI.swift #5 ~Free Talk~」のLTで使用した資料 を元にしています。
LT時は MarkdownをVimで表示した ので、よかったら併せて見てください。用語
本記事で使う用語を説明します。
用語 意味 テストメソッド 自動テストのメソッド テストコード 自動テストのコード プロダクトコード 通常のソースコード。テストコードの対義語として使う あるメソッドのテストケースを考える
ここから本題です。
エンジニアが働けるかどうかを判定するメソッドを用意しました。
やる気が100以上でRed Bullを持っている場合のみ働けます。func canWork(motivation: Int, hasRedBull: Bool) -> Bool { if motivation < 100 { return false } if !hasRedBull { return false } return true }こちらのメソッドのテストケースは全部で何パターン必要でしょうか?
あるメソッドのテストケース数
私が考える正解は、以下の4パターンです。
motivation hasRedBull return value 99 true false 99 false false 100 true true 100 false false
motivation
を境界値分析すると、99
と100
が境界値とわかります。
さらに「100未満」と「100以上」で同値分割できるので、99
と100
の2通りのみテストすれば十分です。
hasRedBull
はBool型なのでtrue
とfalse
の2通りしかありません。2通り × 2通り = 4パターン です。
手動でテストする場合
上記のメソッドを 手動で テストする場合、エクセルなどを使って一覧表に○×を付けるのが一般的だと思います。
先ほどの表に「No」「判定」「日付」「担当」列を追加しました。
No motivation hasRedBull return value 判定 日付 担当 1 99 true false OK 2019/12/14 ウホーイ 2 99 false false OK 2019/12/14 ウホーイ 3 100 true true NG 2019/12/14 ウホーイ 4 100 false false OK 2019/12/14 ウホーイ これで十分であり、表になっているのでパターンが網羅できているかもわかりやすいです。
自動でテストする場合
上記のメソッドを 自動で テストする場合はどうでしょうか?
最も簡単な方法として、1ケース1テストメソッドとして実装することが考えられます。1ケース1テストメソッドとして実装func test_canWork_motivation_99_hasRedBull_true() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: true)) } func test_canWork_motivation_99_hasRedBull_false() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 99, hasRedBull: false)) } func test_canWork_motivation_100_hasRedBull_true() { let logic = LogicSample() XCTAssertTrue(logic.canWork(motivation: 100, hasRedBull: true)) } func test_canWork_motivation_100_hasRedBull_false() { let logic = LogicSample() XCTAssertFalse(logic.canWork(motivation: 100, hasRedBull: false)) }テストケースごとにテストメソッドを実装することがあるかもしれませんが、実は以下のデメリットがあります。
網羅できているかわかりづらい
エクセルのようにテストケースが一覧になっていないので、網羅できているかわかりづらいです。同じような処理で読みづらい
canWork()
メソッドに渡す引数以外は同じ処理なので、どこが違うか注力して読み比べる必要があります。
同じ処理を繰り返すのはDRY原則にも反しています。メソッド名に悩む
テストメソッドの命名はメソッド数が増えるほど難しくなり、testCanWork2()
のように末尾に連番を振ることがあるかもしれません。
今回は引数と値をそのままメソッド名に付けましたが、適切ではない気がします。テストコードがわかりづらいとどうなるか
テストコードの可読性が低い場合、 テストコードが保守されなくなる 可能性が高まります。
テストコードはプロダクトコードに比べて可読性が無視されがちですが、それだとテストが失敗しても直せなくなり、テストが成功しなくてもPRを通すようになります。自動テストはうまく使うとプロダクトコードを安全に破壊できるので、もったいないです。
どうやったらテストコードがわかりやすくなる?
先ほどのデメリットにも上げた通り、 自動テストでもエクセルのように一覧になっていたら わかりやすくなると思いませんか?
自動テストでテストケースを一覧にする// | motivation | hasRedBull | return value | // | ---------: | :--------- | :----------- | // | 99 | true | false | // | 99 | false | false | // | 100 | true | true | // | 100 | false | false | // // ↓ // let testCases: [(motivation: Int, hasRedBull: Bool, expect: Bool)] = [ ( 99, true, false), ( 99, false, false), (100, true, true ), (100, false, false) ]どうでしょうか?
テストケースをタプルで表して配列に入れることで、エクセルのように一覧で見れるようになりました。タプルはtypealiasを使って別名を付けることで、型のように扱えます。
タプルに別名を付けるtypealias TestCase = (motivation: Int, hasRedBull: Bool, expect: Bool) let testCases: [TestCase] = [ ( 99, true, false), ( 99, false, false), (100, true, true ), (100, false, false) ]ちょっとした工夫ですが、より可読性が上がりました。
構造体を定義するより簡単に使えるので、型を使い捨てる場合には便利です。あとは作成した配列をループさせ、1つずつアサーションすればOKです。
テストメソッドの全体は以下の通りです。工夫したテストメソッドの全体func test_canWork() { typealias TestCase = (line: UInt, motivation: Int, hasRedBull: Bool, expect: Bool) let testCases: [TestCase] = [ (#line, 99, true, false), (#line, 99, false, false), (#line, 100, true, true ), (#line, 100, false, false) ] for (line, motivation, hasRedBull, expect) in testCases { let logic = LogicSample() let result = logic.canWork(motivation: motivation, hasRedBull: hasRedBull) XCTAssertEqual(result, expect, line: line) } }
#line
はソースコードの行番号を表し、これを渡すことでテストの失敗時に対象の要素でエラーになってくれます。
ただ、エラーメッセージに変数名が出力されないため、若干わかりづらいです。
いちいち行番号を渡すのも冗長なので、このあたりをいい感じに吸収してくれるライブラリを作りたいです。パラメタライズドテスト
引数のみを切り替えて実行するテストを パラメタライズドテスト といいます。
言語によって呼び方が異なることがあります。
言語 名前 Go テーブル駆動テスト Java (JUnit) パラメータ化テスト 他言語では一般的なのに、なぜかSwiftではあまり知られていません。
以下のようにメリットも多く、簡単に導入できるのでオススメです。
- テストパターンが網羅できているかひと目でわかる
- 1メソッドで済むので処理が重複せず、命名にも困らない
- テストケースを追加・変更・削除しやすい
1行変更するのみおまけ
JavaのテストフレームワークであるSpockには「Data Tables」という機能があり、より直感的にテストケースを記述できます。
Spockによるテストclass MathSpec extends Specification { def "maximum of two numbers"(int a, int b, int c) { expect: Math.max(a, b) == c where: a | b | c 1 | 3 | 3 7 | 4 | 7 0 | 0 | 0 } }Swiftでは、独自演算子とFunction Builderを使って実現できそうな気もします。
強い方教えてください…。おわりに
パラメタライズドテストのメリットが少しでも伝わっていると嬉しいです?
以上、 Swift Advent Calendar 2019 の14日目の記事でした。
15日目は @freddi_ さんの記事です。参考リンク
- Swift で ParameterizedTest をやってみた話/swift-parameterized-test - Speaker Deck
私がパラメタライズドテストを知ったきっかけのスライドです
より詳細に解説されているので、ぜひ読んでください- yumemi-swift-5-sample/LogicSampleTests.swift at master · uhooi/yumemi-swift-5-sample
本記事のサンプルコードです- https://twitter.com/lovee/status/1200595966782406656?s=20
タプルに別名を付けるのは、Twitterでアドバイスを頂きました
- 投稿日:2019-12-15T00:37:46+09:00
Xamarin.Forms でガワネイティブアプリを作るときのテンプレートプロジェクトを作る1
今年は専ら Angular で Webアプリを作ったり、ガワネイティブアプリを作ったりしていますが Xamarin のアドベントカレンダーと聞いてやってきました。
概要
最近は、モバイルネイティブアプリよりも Webアプリ(SPA/PWA)、そしてそれを利用したガワネイティブアプリを推している私ですが、ガワネイティブアプリを作る時の「ガワ」には Xamarin(Xamarin.Forms) を採用しています。
なぜガワネイティブなのか?ネイティブではなく、Web(PWA)でもないのか?については、
のエントリがよく解説されていますのでご一読を。私のケースは BtoC ではなく BtoB であるため、全面的に一致するわけではありませんが、内容については大いに同意できます。
ガワネイティブアプリは、Webアプリがネイティブの機能を欲するから採用されるわけで、それを制御するためにWebアプリとネイティブ機能の相互通信が必要になります。
また、「ガワ」は
WebView
なわけですが、それがアプリとして自然に振る舞うために、いくつかの「設定」をしてあげる必要があります。この記事は、そのような「Xamarin(.Forms) でガワネイティブアプリを作るときのリファレンス」になればよいなと思って書きます。
尚、
Advent Calendar の締め切りに間に合わせるために意外と情報量が多かったので、何回かに分けます。
今回は初回です。目次
- 日本語入力時の画面高さの調整
- ステータスバー、あるいは SafeArea(ノッチ部)の色
- 【次回以降予定】アプリ情報の Web 側への引き渡し
- 【次回以降予定】
<input type="xxx">
への対応- 【次回以降予定】Back ボタンハンドリングの Web 側への移譲
- 【次回以降予定】スプラッシュスクリーンおよび初回読み込み時の対応
1. 日本語入力時の画面高さの調整
ソフトウェアキーボードが、コンテンツの手前が重なってしまう問題の解決です。
これは、
を適用して解決します。
Android の場合
今回のように WebView だけ対応すればよい場合は、
MainActivity.cs
に `.UseWindowSoftInputModeAdjust()
の行を追加してあげればよいみたいです。// MainActivity.cs protected override void OnCreate(Bundle bundle) { TabLayoutResource = Resource.Layout.Tabbar; ToolbarResource = Resource.Layout.Toolbar; base.OnCreate(bundle); global::Xamarin.Forms.Forms.Init(this, bundle); LoadApplication(new App()); Xamarin.Forms.Application.Current.On<Xamarin.Forms.PlatformConfiguration.Android>() .UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize); // ←ここ!! }iOS の場合
iOS の場合、前出のわたしのエントリを改善して頂いた、
があるのですが、WebView だけ対応すればよい場合、特にやることは無いですw
対応結果
これらの対応を行うと、下図のようにソフトウェアキーボードを表示していても、WebView のコンテンツが隠れることはなくなります。
なお、この例では Materialize-CSS の Starter Template を表示させています。
元コンテンツ 対応前 対応後(Android) 対応後(iOS) 2. ステータスバー、あるいは SafeArea(ノッチ部)の色
Android と iOS のステータスバーの色は、Webアプリのテーマ色に合わせたいものです。
また、iOS では SafeArea(ノッチのところ)を除けるような対応が必要になります。ガワのプロジェクト作成直後は下図のように、Android では青系のステータスバーに、iOS では白色になってしまいます。
対応前(Android) 対応前(iOS) この例では Web アプリのテーマ色が緑なので、どちらもステータスバーを緑色にします。
Android の場合
Android プロジェクトにある
Resources/values/styles.xml
のcolorPrimaryDark
の色を修正します。下の例では#66BB6A
に書き換えました。(colorPrimary
も変えておいた方が良いかもしれませんね。)Resources/values/styles.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <style name="MainTheme" parent="MainTheme.Base"> </style> <!-- Base theme applied no matter what API --> <style name="MainTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar"> <!--If you are using revision 22.1 please use just windowNoTitle. Without android:--> <item name="windowNoTitle">true</item> <!--We will be using the toolbar so no need to show ActionBar--> <item name="windowActionBar">false</item> <!-- Set theme colors from https://aka.ms/material-colors --> <!-- colorPrimary is used for the default action bar background --> <item name="colorPrimary">#2196F3</item> <!-- colorPrimaryDark is used for the status bar --> <item name="colorPrimaryDark">#66BB6A</item> ←ここ!!! <!-- colorAccent is used as the default value for colorControlActivated which is used to tint widgets --> <item name="colorAccent">#FF4081</item> <!-- You can also set colorControlNormal, colorControlActivated colorControlHighlight and colorSwitchThumbNormal. --> <item name="windowActionModeOverlay">true</item> <item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item> </style> <style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog"> <item name="colorAccent">#FF4081</item> </style> </resources>iOS の場合
ノッチの対応がちょっと面倒です。
こちらの方法をとります。ノッチ(SafeArea)の領域分のパディングを設ける Effects を作って、Page に適用する方法です。
まずは、Forms の共通プロジェクトに
SafeAreaPaddingEffect
を作成します。SafeAreaPaddingEffect.cs
using System; using Xamarin.Forms; namespace GawaNativeGettingStarted { public class SafeAreaPaddingEffect : RoutingEffect { public SafeAreaPaddingEffect() : base("GawaNativeGettingStarted.SafeAreaPaddingEffect") { } } }先に
MainPage.xaml
に適用しちゃいましょう。
尚、BackgroundColor="#66BB6A"
で指定した色が、Web アプリ側の緑のテーマ色です。MainPage.xaml
<?xml version="1.0" encoding="utf-8"?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:local="clr-namespace:GawaNativeGettingStarted;assembly=GawaNativeGettingStarted" xmlns:effect="clr-namespace:GawaNativeGettingStarted" x:Class="GawaNativeGettingStarted.MainPage"> <StackLayout Orientation="Vertical" BackgroundColor="#66BB6A"> <StackLayout.Effects> <effect:SafeAreaPaddingEffect /> </StackLayout.Effects> <WebView HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" Source="https://eeaab7d6.ngrok.io"/> </StackLayout> </ContentPage>次に iOS のプロジェクトにも
SafeAreaPaddingEffect
を作成し、SafeArea 考慮の実装をします。using GawaNativeGettingStarted.iOS.Effects; using UIKit; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; [assembly: ResolutionGroupName("GawaNativeGettingStarted")] [assembly: ExportEffect(typeof(SafeAreaPaddingEffect), nameof(SafeAreaPaddingEffect))] namespace GawaNativeGettingStarted.iOS.Effects { class SafeAreaPaddingEffect : PlatformEffect { Thickness _padding; protected override void OnAttached() { if (Element is Layout element) { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) { _padding = element.Padding; var insets = UIApplication.SharedApplication.Windows[0].SafeAreaInsets; // Can't use KeyWindow this early if (insets.Top > 0) // We have a notch { element.Padding = new Thickness(_padding.Left + insets.Left, _padding.Top + insets.Top, _padding.Right + insets.Right, _padding.Bottom); return; } } // Uses a default Padding of 20. Could use an property to modify if you wanted. element.Padding = new Thickness(_padding.Left, _padding.Top + 20, _padding.Right, _padding.Bottom); } } protected override void OnDetached() { if (Element is Layout element) { element.Padding = _padding; } } } }これは前出のリンク先の内容そのものです。
ここまでの対応で、SafeArea 分の余白を設け、背景色を Web アプリ側に合わせることができました。
対応中(iOS) しかし、ステータスバーの文字色が黒になっています。これを白にしましょう。
参考になるエントリはこちら↓です。これを参考に iOS プロジェクトの
info.plist
を編集し、次のエントリを追加します。
- Status bar style : White
- View controller-based status bar appearance : No
これで、ステータスバーの文字色が白になります。
対応結果
対応後の見た目はこんな感じになります。結構ネイティブアプリっぽくなって来たでしょう?
対応後(Android) 対応後(iOS) まとめ
とりあえず初回ということで、見た目中心の対応内容を挙げてみました。
一通り揃ったら、GitHub にサンプルを上げて、あわよくば可能なところは nuget パッケージに切り出して利用できるようにしていきたいと思います。見た目の対応でも、ダークテーマ対応などはまったく無知なのでこれから勉強していきます。
次回エントリは、、、年内いける・・・かな?参考(次回以降のも含む)
- 投稿日:2019-12-15T00:32:38+09:00
iPhone で交通系IC(Suica、PASMO、ICOCA、…etc.)を読み取ってみよう!
運転免許証、物販向け電子マネー(楽天Edy、nanaco、WAON)に引き続き、今回は交通系ICである Suica、PASMO、ICOCA などの残高を iPhone で読み取ってみましょう!
環境
- 開発
- Xcode Version 11.3 (11C29)
- Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15).
- macOS Catalina 10.15.2(19C57)
- 実機
- iPhone 11 Pro (A2215、MWCC2J/A)
- iOS 13.3 (17C54)
実行結果
Xcode プロジェクトの作成
Create a new Xcode project から iOS の Single View App を作成します。
Capability と Entitlements の設定
Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
すると、「<Product-Name>.entitlements」というファイルが追加されているので、「Near Field Communication Tag Reader Session Formats」に「NFC Data Exchange Format」および「NFC tag-specific data protocol」があることを確認します。Info.plist の設定
Info.plist に「Privacy - NFC Scan Usage Description」と「ISO18092 system codes for NFC Tag Reader Session」を追加します。
「Privacy - NFC Scan Usage Description」には NFC を何のために使用するのかについての説明を、「ISO18092 system codes for NFC Tag Reader Session」の配下には以下の FeliCa システムコードを記述します。
0003
ファイルツリー
今回のプロジェクトのファイルツリーを確認しておきます。
前回の記事と全く同じです。処理は全てViewController.swift
に記述します。コーディング
ライブラリを導入するか、導入せずに直接 Core NFC を操るかの2択になります。。
ライブラリを導入する場合
前回の記事でも登場した、私が開発・公開しているライブラリ、treastrain/TRETJapanNFCReader は交通系ICの残高の取得にも使用できます。導入方法は README をご覧いただくとして、導入後のサンプルコードを記載します。
treastrain/TRETJapanNFCReaderを使った場合import UIKit import TRETJapanNFCReader class ViewController: UIViewController, FeliCaReaderSessionDelegate { var reader: TransitICReader! override func viewDidLoad() { super.viewDidLoad() self.reader = TransitICReader(viewController: self) self.reader.get(itemTypes: [.balance]) } func feliCaReaderSession(didRead feliCaCard: FeliCaCard) { let transitICCard = feliCaCard as! TransitICCard let balance = transitICCard.data.balance! // カード残高 } }直接 Core NFC を使う場合
こちらも前回の記事とコードの内容はほぼ同一ですので、コードの各部分の説明は前回の記事を参考にしてください。異なる部分のみ解説します。
ViewController.swiftimport UIKit import CoreNFC class ViewController: UIViewController, NFCTagReaderSessionDelegate { var session: NFCTagReaderSession? override func viewDidLoad() { super.viewDidLoad() guard NFCTagReaderSession.readingAvailable else { print("NFC タグの読み取りに非対応のデバイス") return } self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self) self.session?.alertMessage = "カードの上に iPhone の上部を載せてください" self.session?.begin() } func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) { print("tagReaderSessionDidBecomeActive(_:)") } func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { let readerError = error as! NFCReaderError print(readerError.code, readerError.localizedDescription) } func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { print("tagReaderSession(_:didDetect:)") let tag = tags.first! session.connect(to: tag) { (error) in if let error = error { session.invalidate(errorMessage: error.localizedDescription) return } guard case NFCTag.feliCa(let feliCaTag) = tag else { session.invalidate(errorMessage: "FeliCa ではない") return } session.alertMessage = "カードを読み取っています…" let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined() print("IDm:", idm) /// FeliCa サービスコード let serviceCode = Data([0x00, 0x8B].reversed()) /// ブロック数 let blocks = 1 let blockList = (0..<blocks).map { (block) -> Data in Data([0x80, UInt8(block)]) } feliCaTag.readWithoutEncryption(serviceCodeList: [serviceCode], blockList: blockList) { (status1, status2, blockData, error) in if let error = error { session.invalidate(errorMessage: error.localizedDescription) return } guard status1 == 0x00, status2 == 0x00 else { print("ステータスフラグがエラーを示しています", status1, status2) session.invalidate(errorMessage: "ステータスフラグがエラーを示しています s1:\(status1), s2:\(status2)") return } let data = blockData.first! let balance = data.toIntReversed(11, 12) print(data as NSData) print("残高: ¥\(balance)") session.alertMessage = "残高: ¥\(balance)" session.invalidate() } } } } extension Data { /// https://github.com/treastrain/TRETJapanNFCReader/blob/master/TRETJapanNFCReader/Extensions.swift#L112 func toIntReversed(_ startIndex: Int, _ endIndex: Int) -> Int { var s = 0 for (n, i) in (startIndex...endIndex).enumerated() { s += Int(self[i]) << (n * 8) } return s } }コード解説
前回の記事を参照
⏩ 対応端末かどうかを確かめる
⏩NFCTagReaderSession
をスタートさせる
⏩NFCTagReaderSessionDelegate
⏩ タグに接続する
⏩ Read Without Encryption コマンドを送信FeliCa サービスコードの指定
/// FeliCa サービスコード let serviceCode = Data([0x00, 0x8B].reversed()) /// ブロック数 let blocks = 1 let blockList = (0..<blocks).map { (block) -> Data in Data([0x80, UInt8(block)]) }交通系ICカードの残高情報は FeliCa サービスコード
0x008B
に1ブロックで記録されています。リトルエンディアンで送信するために.reversed()
しています。データの解析
let data = blockData.first! let balance = data.toIntReversed(11, 12) print(data as NSData) print("残高: ¥\(balance)") session.alertMessage = "残高: ¥\(balance)" session.invalidate()Read Without Encryption コマンドの結果として返ってきた
blockData
の要素数は1です。
その唯一の要素であるdata
の11番目と12番目の2ビットにリトルエンディアンで残高が記録されています。
NFC の通信処理をすべて終えたらsession.invalidate()
で終了します。後記
内容としては楽天Edy、nanaco、WAON の読み取りとほぼ同一で、指定する FeliCa システムコード、FeliCa サービスコードが異なるというものでした。
都市部であればどうしても交通系ICが生活の一部になっているかと思いますので、あえて別記事として執筆しました。
自分のいつも使う交通系ICと iPhone でちょっと遊んでみませんか…?
- 投稿日:2019-12-15T00:23:46+09:00
【Unity+ARKit3(+PeopleOcclution)】カラスがゴミ袋を回収するクソアプリを作る
この記事はクソアプリ Advent Calendar 2019 の15日目の記事です。
クソアプリクリエイターの皆さんが知見や学びの深い全然クソじゃないアプリを皆さん生み出していく中、
自分のためだけに勉強がてら新しいiPhone使って作りたいものを作りましたARで認識した床面をタップすると、ゴミ袋が出てきてカラスがせっせと回収に行くアプリです。
ゴミ袋を回収すると、ポイントカウントしていきます。
子供には好評でした。今回制作したものは時短でやろうと思い、Asset Storeの使用が多いのですが、
Asset Store関連のものを覗いたプロジェクトデータをGithubにアップしたので、合わせて見てみてください。
https://github.com/sadakitchen/ARFoundationTest/tree/master環境
- macOS Catalina 10.15.2
- Xcode 11.3
- Unity 2019.2.2f1
- iPhone 11 Pro(iOS 13.2.3)
使用アセット
- KUBIKOS - Animated Cube Mini BIRDS
キューブ状がかわいい鳥の3Dモデル。1mベースのキューブなのでARでも扱いやすいです(?)- DoozyUI: Complete UI Management System
UI周りのスクリプトを簡単にかつGUIで色々できる。今回はSoundyを使いました。- Puzzle Audio Kit (Music + FX)
サウンドアセット。アイテム取ったときのサウンドに使いました。参考
- 実践ARKit
https://booth.pm/ja/items/1038241- Github:Unity-Technologies/arfoundation-samples
https://github.com/Unity-Technologies/arfoundation-samples- UnityのAR FundationでPeopleOcclusionしてみた
https://qiita.com/Tanktop_in_Feb/items/55201612a8b449800100経緯とアプローチ
手持ちのiPhone Xがグリーンスクリーンになってしまい、iPhone 11 Proを購入したのですが、
前々から気になっていた ARKit3 を試したかったためなにか作ろうと思い立ちました。まず、 ARKit について体系的に知りたかったので、
@shu223 さんの「実践ARKit」でサンプル動かしてざっと眺めることに。
どんな仕組みで動いているのかも詳細に書かれているのでオススメです。
次に Unity で Unity-Technologies/arfoundation-samples のサンプルシーンをビルドすることに。
が、ここでmacOS Catalinaユーザーへの罠があり・・・
CatalinaとUnityの最新バージョン(2019/12/15現在 2019.2.15f1)で
ビルドがうまく行かないエラーに見舞われてしまうことに・・・しばらく情報を探し回ってたところ、ここのスレッドで Unity 2019.2.2f1 だとうまくビルドできたという記事を発見。
https://forum.unity.com/threads/unknown-shader-compiler-error-using-unity-2019-2-8f1-when-building-ios.758339/Unity 2019.2.2f1にて、
Player Settings の Other Settings の Color Space を Linearに変更、
Auto Graphics API の チェックをOFFにし、Graphics APIs にて Metal を最上位に設定。ちなみになぜかBuild And Runが動作しないので、
一度BuildでXcodeプロジェクトファイルを作り、Unity-iPhone.xcodeproj を起動するフローでした。ひとしきり触った後は、技術選定を考えました。
ARKit3 の特徴は以下とのことで、
- People Occlution(人物の奥行きを加味したAR合成)
- Motion Capture(人物のスケルトンを取得)
- Simultaneous Front and Back Camera(フロントカメラとバックカメラ)
- Multiple Face Tracking(複数顔認識)
- Collaborative Sessions(ARWorldMapの共有機能強化)
今回はググるとサンプルが沢山あって困りにくそうなPeopleOcclutionを使ってみようと思いました
制作のポイント
前述のUnityのAR FundationでPeopleOcclusionしてみたが非常に詳しく解説しているので、
ポイントを要所要所解説します。1 . Package Managerから必要なものをDL
- ARFoundation 3.0.0 preview.6
- ARKit XR Plugin 3.0.0 preview.4
※ 2019.12.14現在 ARFoundation 3.0.1 で ARHumanBodyManager (人体検出関連クラス)が存在しないようです。2 . シーン上にGameObject > XR から ARSession、ARSessionOrigin、ARDefaultPlane を追加。
ARDefaultPlaneはPrefab化し、シーンから削除しておきます。3 . ARSessionOrigin に ARPlaneManager、ARHumanBodyManagerコンポーネントを追加。
ARPlaneManagerコンポーネントのPlane PrefabにPrefab化しておいたARDefaultPlaneを設定し、
ARHumanBodyManagerコンポーネントの設定を、
Human Segmented Stencil:Full Screen Resolution、
Human Segmented Depth:Standard Resolution
に設定します。4 . PeopleOcclusionPostEffect.csとPeopleOcclusion.shaderを作成します。
UnityのAR FundationでPeopleOcclusionしてみたのコードをベースに以下のように変更しました。PeopleOcclusion.shaderv2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; if(_ONWIDE == 1) { o.uv1 = float2(v.uv.x, (1.0 - (_UVMultiplierLandScape * 0.5f)) + (v.uv.y / _UVMultiplierLandScape)); o.uv2 = float2(lerp(1.0 - o.uv1.x, o.uv1.x, _UVFlip), lerp(o.uv1.y, 1.0 - o.uv1.y, _UVFlip)); } else { o.uv1 = float2(1.0 - v.uv.y, 1.0 - _UVMultiplierPortrait * 0.5f + v.uv.x / _UVMultiplierPortrait); float2 oUV1_f = float2((1.0 - (_UVMultiplierPortrait * 0.5f)) + (v.uv.x / _UVMultiplierPortrait), v.uv.y); o.uv2 = float2(lerp(1.0 - oUV1_f.y, oUV1_f.y, 0), lerp(oUV1_f.x, 1.0 - oUV1_f.x, 1)); } return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); fixed4 cameraFeedCol = tex2D(_CameraFeed, i.uv1); float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv)); float4 stencilCol = tex2D(_OcclusionStencil, i.uv2); float occlusionDepth = tex2D(_OcclusionDepth, i.uv2) * 0.625; //0.625 hack occlusion depth based on real world observation float showOccluder = step(occlusionDepth, sceneDepth) * stencilCol.r; // 1 if (depth >= ocluderDepth && stencil) return lerp(col, cameraFeedCol, showOccluder); }PeopleOcclusionPostEffect.cs// ...中略 [SerializeField] private Shader m_peopleOcclusionShader = null; //[SerializeField] Texture2D testTexture; // 削除 // ...中略 private void RefreshCameraFeedTexture() { // ...中略 m_cameraFeedTexture.Apply(); m_material.SetTexture("_CameraFeed", m_cameraFeedTexture); // testTextureをm_cameraFeedTextureに変更 }※Player Settings から Other Settings の Allow 'Unsafe' Code のチェックをONにしておきます。
シーン上のARCameraにPeopleOcclusionPostEffectをアタッチし以下のように設定します。
5 . 続いて、カメラで表示されている空間にオブジェクトを表示することができるようにSpawn.csを作成します。
Spawn.csとARRaycastManagerコンポーネントをAR Session Originへアタッチ。
Spawnのm_SpawnPrefabには表示したいオブジェクトを適当に用意してアタッチできます。Spawn.csusing System.Collections.Generic; using UnityEngine; using UnityEngine.XR.ARFoundation; using UnityEngine.XR.ARSubsystems; [RequireComponent(typeof(ARRaycastHit))] public class Spawn : MonoBehaviour { [SerializeField] GameObject m_SpawnPrefab; private readonly List<ARRaycastHit> _hitResults = new List<ARRaycastHit>(); private ARRaycastManager _rayManage; private void Awake() { _rayManage = this.GetComponent<ARRaycastManager>(); } private void Update() { if (Input.GetMouseButtonDown(0)) { if (_rayManage.Raycast(Input.GetTouch(0).position, _hitResults, TrackableType.PlaneWithinPolygon)) { Instantiate(m_SpawnPrefab, _hitResults[0].pose.position, Quaternion.identity); } } } }6 . ゴミ袋に向かっていくためのSpawnTrace.csを作成します。
指定したTagNameからオブジェクトを検索し、その中から最も近いオブジェクトに向かって行く処理にしています。
TagNameはTrashBagとしましたが、任意の名前で大丈夫です。SpawnTrace.csusing UnityEngine; public class SpawnTrace : MonoBehaviour { [SerializeField] private GameObject m_Tracer; [SerializeField] private string m_TagName; [SerializeField] private float speed = 0.1f; private GameObject _targetObject; void Update() { _targetObject = GetTargetObject(m_TagName); if (_targetObject == null) return; Vector3 relativePos = _targetObject.transform.position - m_Tracer.transform.position; Quaternion rotation = Quaternion.LookRotation(relativePos); m_Tracer.transform.rotation = Quaternion.Slerp(m_Tracer.transform.rotation, rotation, speed); m_Tracer.transform.position = Vector3.MoveTowards(m_Tracer.transform.position, _targetObject.transform.position, speed * Time.deltaTime); } private GameObject GetTargetObject(string tagName) { float nearDis = 0; GameObject targetObj = null; foreach (GameObject obs in GameObject.FindGameObjectsWithTag(tagName)) { var tmpDis = Vector3.Distance(obs.transform.position, m_Tracer.transform.position); if (nearDis == 0 || nearDis > tmpDis) { nearDis = tmpDis; targetObj = obs; } } return targetObj; } }今回はCrowというGameObjectを作成し、それにSpawnTrace.csをアタッチしています。
7 . ゴミ袋と衝突したときゴミ袋を消去し、数えるCounter.csを作成します。
作成後、前述のCrowにCounter.cs、Rigidbodyコンポーネントをアタッチします。Counter.csusing UnityEngine; using UnityEngine.UI; public class Counter : MonoBehaviour { [SerializeField] private string m_TagName; [SerializeField] private Text m_ScoreText; private int _score = 0; void OnTriggerEnter(Collider collision) { if (collision.gameObject.CompareTag(m_TagName)) { Destroy(collision.gameObject); _score++; m_ScoreText.text = _score.ToString(); } } }10 . Buildして確かめてみます。
Player Settings の Other Settings より以下設定を行います。
Camera Usage Description : (任意の文字列)
Target minimum iOS Version : 13.0
Architecture : ARM64まとめ
ネイティブでの制作も検討したのですが、
Unityを利用することでAsset Storeを使うことができるメリットが大きかったため、
目的を最も素早く達成できる Unity を選択しました。クソアプリ制作は、どんなクオリティでも許される免罪符的なイベントだと思います。
腰の重い人にピッタリなイベントです。
- 投稿日:2019-12-15T00:15:14+09:00
忙しい人のためのCore Haptics
はじめに
iOS10からUIKitにはUIFeedbackGeneratorというAPIが追加されており、簡単にHapticFeedbackを実装することができました。
iOS13が公開され、UIFeedbackGeneratorに比べてカスタマイズできるHaptic Feedbackを作成できるようになったのですが、UIFeedbackGeneratorがあるUIKitにではなく、新しくCore HapticsというFrameworkが追加されました。
この記事はCore Hapticsとはどういう物なのか、実装方法などを簡単にまとめてみました。
HapticFeedbackとは
UISwitchでON/OFFを切り替えたり、UISliderでSliderを端までスワイプしたり、UIDatePickerでスクロールした時などの特定のアクションを行うと起きる振動のことで、たくさんの場所に組み込まれており、いくつかの種類が存在します
Core Hapticsとは
対応機種
iPhone8以降の全機種で使用することができます。
UIFeedBackGeneratorとCore Haptics
UIFeedBackGeneratorはNotification,Impact,Selectionなど、アクションを起こしたユーザーに対してFeedBackを行うことにより、UXを向上させることが目的とされています。
対して、Core Hapticsは自分でHapticFeedBackを内容、使うタイミングなどを自由に設定したい場合に使用します。
他のAPI(Core Animation, AVAudioEngine)との連動も可能なことや、Core HapticsはAudioAPIであり、今までAppleが実装していた音と連動して使用することなど(例:UIDatePickerのスクロール)も可能なので、UIFeedBackGeneratorに比べて豊かな表現を行うことができます。WWDCでは特に活用できるジャンルとして、ゲーム、ARなどが挙げられていました・
コード
それでは実際に実装しながら見ていきたいと思います。
モジュールのインポート
import CoreHapticsHapticEngine作成
var hapticEngine: CHHapticEngine? do { hapticEngine = try CHHapticEngine() try hapticEngine?.start() } catch { print("Error \(error)") }・CHHapticEngineには
capabilitiesForHardware
というメソッドが存在しており、HapticEngineを作成する前にデバイスが実行されているデバイス環境でHapticsFeedBackがサポートされているか確認することもできます。guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }・そのほかにも
stoppedHandler
,resetHandler
が用意されており、アプリケーション以外の動作でエンジンが終了される時などに使用できるコールバックも用意されています。var stoppedHandler: CHHapticEngine.StoppedHandler { get set } var resetHandler: CHHapticEngine.ResetHandler { get set }HapticEventParameterを元にHapticEventを作成
再生するコンテンツを作成します
//EventParameterを作成 let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1) //EventParameter,EventType,Time,からEventを作成します。 let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0)EventParameterを作成
・Eventで使われるParameterを作成します、ParameterTypeとしては音の強さ、明瞭さなど複数の種類が存在します。
EventParameter,EventType,Time,からEventを作成します。
・先ほど作成したEventParameter,EventType,Time,からEventを作成、eventTypeとしては単発で短い動き、継続的、音とHapticFeedBackを連動させるなどがあります。
再生
do { //先ほど作ったEventからPatternを作成 let hapticPattern = try CHHapticPattern(events: [event], parameters: []) //PatternのPlayerを作成 let hapticPlayer = try hapticEngine?.makePlayer(with: hapticPattern) try hapticPlayer?.start(atTime: CHHapticTimeImmediate) } catch let error{ print("Error: \(error)") }・イベントを1つのPatternとしてラップして、そのPatternに対するPlayerを作成します。
これがCoreHapticsの基本的なコードになります
Dynamic Parameter
UIFeedBackGeneratorとの違いの1つとして、Event Parameterの値を増減することや複数の動きを組み合わせる事が可能です。
・上記のCHHapticEventは単純な振動のみですが、下記のようにするとrelativeTimeを元にタイムラインを設定することにより、複数イベントを作成して、パターンとして扱うこともできます。
//単発のEvent let short1 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0) let short2 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.2) let short3 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.4) let short4 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.6) let short5 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 0.8) let short6 = CHHapticEvent(eventType: .hapticTransient, parameters: [], relativeTime: 1.0) //継続的なEvent let long1 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.2, duration: 0.5) let long2 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 1.8, duration: 0.5) let long3 = CHHapticEvent(eventType: .hapticContinuous, parameters: [], relativeTime: 2.4, duration: 0.5)最後に
今回紹介したのはCore Hapticsの簡単な部分にすぎません、他のPatternの作成方法として、APHP(Apple Haptic Audio Pattern)と呼ばれるファイルを用いてコードからHapticPatternを分離させる方法や、CHHapticParameterCurveといった再生中に値を増減させるなどより自由度の高い作成方法もありますので、気になった方はWWDCの動画やDocumentなどをご覧になってみてください。
ありがとうございました。
参考リンク
- 投稿日:2019-12-15T00:09:07+09:00
FirebaseとUnityの連携 入門(Cloud Storage編)
この記事は、Firebase Advent Calendar 2019の15日目の記事です。
概要
以前、『FirebaseとUnityでアプリ開発(ハンズオンみたいなやつ)』という記事をアップし、そこでFirebaseとUnityの連携方法を簡単に解説しました。
今回は特定のFirebaseのサービスを扱う際、まずは0から作るのではなく、サンプルを活用してFirebaseとUnityの連携をいち早く体験できる方法のご紹介です。
Unityの場合、AssetBundleをサーバに配置し、
UnityWebRequest.Get
などを使ってAssetBundleデータをダウンロードしますよね。
そのため今回は、Cloud Storageを触ってみることで、ゆくゆくAssetBundleデータをFirebaseで管理する設計イメージができないか、まずはやってみましょう。環境
- MacBookPro Mojave 10.14
- Unity 2019.2.9f1
- Firebase for Unity 6.3.0
セットアップ
まずは、導入方法を参考にFirebaseとUnityの連携準備は済ませておきましょう。
そしてサンプルコードとして公式が用意しているfirebase/quickstart-unityのCloud Storage for Firebase Quickstartを活用します。
一見、FirebaseもUnityも古いバージョンで作られていますが、大幅な変更がない最低限の機能はちゃんと動くので、本格的なアプリへの導入の際の設計・開発で参考にしていきましょう。
Unity側の調整
ですが、活用すると言ってもquickstart-unity/storage/testapp/Assets/Firebase/Sample/Storage/UIHandler.cs
だけ扱うので、クラス名だけ変えてC#スクリプトを作成し、コピペしましょう。その後、コピペして作ったC#を空のGameObjectにアタッチし、そのInspector上に表示されるGUISkin変数にGUISkinを作成してアタッチしましょう。
また、カメラの調整を
Skybox
からSolid Color
に変更して、サンプルが分かりやすいように調整しておきましょう。Cloud Storageの設定
次にFirebaseコンソール側の作業になります。
Storageのメニューを開き、事前に画像などをアップし、詳細上から画像のリンクをコピーなどしてメモしておきましょう。
次にルールの設定です。
公式の『Storage セキュリティ ルールを使ってみる』に各ルールの設定サンプルがあり、Authを扱わないので今回は公開のルールを扱います。
この公開のルールは、誰でも読み込みと書き込みが可能な設定なので作業終了後に設定を戻しておくようにしておきましょう(自己責任でお願いします?)。実行
Local File Path
とStorage Location
の設定を先ほどメモした内容に書き換えてDownload Bytes
/Download Stream
/Download to File
の各ボタンを押してみると以下のようになります。成功せず、もしパーミッションエラーで403がある場合は、おそらくルールの設定変更忘れだと思います。
また、Local File Path
の変更を忘れているとデフォルトで設定されているdownloaded_file.txt
の名前でファイルが生成されてしまいます。さいごに
あとはAssetBundleさえ準備できればいつでもFirebaseで管理できるようになりそうですね。
ちなみにCloud Storageのファイルサイズには上りと下りで制限があるのか気になりましたでしょうか。
『Storage セキュリティ ルールを使ってみる』を読んでいるとルール側でデータサイズを指定して上り下りの制御ができるようです。ちょっと調べてみたところデータサイズの制限は特にないようなので、普通にAssetBundleのサーバとして扱えそうですよね^^
- 投稿日:2019-12-15T00:01:03+09:00
Swiftは / 2 より * 0.5 のほうが速いのか
この記事は、CAM Advent Calendar 15日目の記事です
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。概要
以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。
ここ、
/ 2
になっとるけど* 0.5
のほうが速いで確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。
ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。SIL
注目すべきはSILです。
SILはSwift Intermediate Language
の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、raw SIL
とcanonical SIL
があります。
raw SIL
は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL
は、raw SIL
を最適化された状態の SILです。検証1
sample1.swiftlet aaa: Double = 200 let result: Double = aaa * 0.5sample2.swiftlet aaa: Double = 200 let result: Double = aaa / 2実行コード
$ swiftc -emit-sil -O sample1.swift > sample1.sil $ swiftc -emit-sil -O sample2.swift > sample2.sil
-emit-sil
でcanonical SIL
が生成されます。raw SIL
を生成するには、-emit-silgen
と書けばよいです。
-O
が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、-Onone
と同等です。結果
sample1.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample53aaaSdvp : $Double // result sil_global hidden [let] @$s7sample56resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample53aaaSdvp // id: %2 %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample56resultSdvp // id: %7 %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'sample2.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample63aaaSdvp // id: %2 %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample66resultSdvp // id: %7 %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'Diff
@@ -9,21 +9,21 @@ @_hasStorage @_hasInitialValue let result: Double { get } // aaa -sil_global hidden [let] @$s7sample53aaaSdvp : $Double +sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result -sil_global hidden [let] @$s7sample56resultSdvp : $Double +sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample53aaaSdvp // id: %2 - %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 + alloc_global @$s7sample63aaaSdvp // id: %2 + %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 - alloc_global @$s7sample56resultSdvp // id: %7 - %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 + alloc_global @$s7sample66resultSdvp // id: %7 + %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11
float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100
これ、200 / 2
した100
を直接 result に突っ込んでますね……。
最適化された結果、SIL時点で実行時に計算されるのではなく結果のみを返されるようになりました。
リテラル値で計算されるから良いように最適化されたのかなと思ったので、検証コードを変更してみます。検証2
sample3.swiftfunc test(_ value: Double) -> Double { return value * 0.5 } let result: Double = test(200.0)sample4.swiftfunc test(_ value: Double) -> Double { return value / 2 } let result: Double = test(200.0)結果
sample3.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample36resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample36resultSdvp // id: %2 %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample34testyS2dF'sample4.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample46resultSdvp // id: %2 %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample44testyS2dF'Diff
@@ -9,13 +9,13 @@ @_hasStorage @_hasInitialValue let result: Double { get } // result -sil_global hidden [let] @$s7sample36resultSdvp : $Double +sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample36resultSdvp // id: %2 - %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 + alloc_global @$s7sample46resultSdvp // id: %2 + %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 @@ -25,16 +25,16 @@ } // end sil function 'main' // test(_:) -sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { +sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 - %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 + %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 - %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 + %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 -} // end sil function '$s7sample34testyS2dF' +} // end sil function '$s7sample44testyS2dF'%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5割り算と掛け算がちゃんと別れています。
/ 2
を自動的に* 0.5
に変換される、なんてことはないようです。
ちなみに、test(:)
に@inlinable
を追加したら、検証1と同じ計算した結果の100の値が保持されました。展開されて、最適化が施されたようです。まとめ
- リテラル値同士の計算は 最適化されて計算した結果のみ保持する
これを色々調べて思ったのは、ただ早いから
* 0.5
を選ぶのは軽率かなと思いました。可読性の問題もあったり、上の最適化されて結果同じ場合もあったりするので、そこらへんを正しく見極めて書いていくことが大事だと感じました。次は16日目、@keitatata による redis レプリケーションとシャーディング です。お楽しみに。
参考資料
以下の資料は今回の記事を書く上で非常に参考になりました。ありがとうございます。
http://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf
https://blog.waft.me/2018/01/09/swift-sil-1/
https://github.com/apple/swift/blob/master/docs/SIL.rst
https://qiita.com/Kuniwak/items/cbf6b88db249838895b5
- 投稿日:2019-12-15T00:01:03+09:00
最適化から見る、Swiftの / 2 と * 0.5
この記事は、CAM Advent Calendar 15日目の記事です
前回は @matsuhei さんによる、 レガシーコードに対する解析ツール(重複コード編)でした。概要
以前、Swiftのコードを誰かに見せてたときに以下のように指摘されました。
ここ、
/ 2
になっとるけど* 0.5
のほうが速いで確かに、基本的には割り算よりも掛け算のほうがパフォーマンスがいいです。直に頭で計算するとき、僕も掛け算のほうが計算しやすいです。計算理由にもよりますが、Swiftもその例から漏れないはずです。
ただ、iOSでは本番用のipaを作成するときにはリリースビルド(本番)が行われます。
最適化の側面から見て、コードが一緒になってないかな?と思ったので、検証してみました。SIL
注目すべきはSILです。
SILはSwift Intermediate Language
の略で、実行できるバイナリーに落とし込むために必要な中間言語です。
Swift CompilerはLLVM IRに落とし込む前にSILを生成します。
SILは主に2種類、raw SIL
とcanonical SIL
があります。
raw SIL
は、AST(abstract syntax tree)から解析しやすいように変換された直後のものです。
canonical SIL
は、raw SIL
を最適化された状態の SILです。検証1
sample1.swiftlet aaa: Double = 200 let result: Double = aaa * 0.5sample2.swiftlet aaa: Double = 200 let result: Double = aaa / 2実行コード
$ swiftc -emit-sil -O sample1.swift > sample1.sil $ swiftc -emit-sil -O sample2.swift > sample2.sil
-emit-sil
でcanonical SIL
が生成されます。raw SIL
を生成するには、-emit-silgen
と書けばよいです。
-O
が付いていますが、これは canonical SIL からまた最適化が施された状態のものを生成するために必要なオプションで、ついてない場合は、-Onone
と同等です。結果
sample1.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample53aaaSdvp : $Double // result sil_global hidden [let] @$s7sample56resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample53aaaSdvp // id: %2 %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample56resultSdvp // id: %7 %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'sample2.silsil_stage canonical import Builtin import Swift import SwiftShims @_hasStorage @_hasInitialValue let aaa: Double { get } @_hasStorage @_hasInitialValue let result: Double { get } // aaa sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample63aaaSdvp // id: %2 %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 alloc_global @$s7sample66resultSdvp // id: %7 %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11 %12 = integer_literal $Builtin.Int32, 0 // user: %13 %13 = struct $Int32 (%12 : $Builtin.Int32) // user: %14 return %13 : $Int32 // id: %14 } // end sil function 'main'Diff
@@ -9,21 +9,21 @@ @_hasStorage @_hasInitialValue let result: Double { get } // aaa -sil_global hidden [let] @$s7sample53aaaSdvp : $Double +sil_global hidden [let] @$s7sample63aaaSdvp : $Double // result -sil_global hidden [let] @$s7sample56resultSdvp : $Double +sil_global hidden [let] @$s7sample66resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample53aaaSdvp // id: %2 - %3 = global_addr @$s7sample53aaaSdvp : $*Double // user: %6 + alloc_global @$s7sample63aaaSdvp // id: %2 + %3 = global_addr @$s7sample63aaaSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4069000000000000 // 200 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 - alloc_global @$s7sample56resultSdvp // id: %7 - %8 = global_addr @$s7sample56resultSdvp : $*Double // user: %11 + alloc_global @$s7sample66resultSdvp // id: %7 + %8 = global_addr @$s7sample66resultSdvp : $*Double // user: %11 %9 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %10 %10 = struct $Double (%9 : $Builtin.FPIEEE64) // user: %11 store %10 to %8 : $*Double // id: %11
float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100
これ、200 / 2
した100
を直接 result に突っ込んでますね……。
最適化された結果、SIL時点で実行時に計算されるのではなく結果のみを返されるようになりました。
リテラル値で計算されるから良いように最適化されたのかなと思ったので、検証コードを変更してみます。検証2
sample3.swiftfunc test(_ value: Double) -> Double { return value * 0.5 } let result: Double = test(200.0)sample4.swiftfunc test(_ value: Double) -> Double { return value / 2 } let result: Double = test(200.0)結果
sample3.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample36resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample36resultSdvp // id: %2 %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample34testyS2dF'sample4.silsil_stage canonical import Builtin import Swift import SwiftShims func test(_ value: Double) -> Double @_hasStorage @_hasInitialValue let result: Double { get } // result sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): alloc_global @$s7sample46resultSdvp // id: %2 %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 %7 = integer_literal $Builtin.Int32, 0 // user: %8 %8 = struct $Int32 (%7 : $Builtin.Int32) // user: %9 return %8 : $Int32 // id: %9 } // end sil function 'main' // test(_:) sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 } // end sil function '$s7sample44testyS2dF'Diff
@@ -9,13 +9,13 @@ @_hasStorage @_hasInitialValue let result: Double { get } // result -sil_global hidden [let] @$s7sample36resultSdvp : $Double +sil_global hidden [let] @$s7sample46resultSdvp : $Double // main sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 { bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>): - alloc_global @$s7sample36resultSdvp // id: %2 - %3 = global_addr @$s7sample36resultSdvp : $*Double // user: %6 + alloc_global @$s7sample46resultSdvp // id: %2 + %3 = global_addr @$s7sample46resultSdvp : $*Double // user: %6 %4 = float_literal $Builtin.FPIEEE64, 0x4059000000000000 // 100 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 store %5 to %3 : $*Double // id: %6 @@ -25,16 +25,16 @@ } // end sil function 'main' // test(_:) -sil hidden @$s7sample34testyS2dF : $@convention(thin) (Double) -> Double { +sil hidden @$s7sample44testyS2dF : $@convention(thin) (Double) -> Double { // %0 // users: %3, %1 bb0(%0 : $Double): debug_value %0 : $Double, let, name "value", argno 1 // id: %1 - %2 = float_literal $Builtin.FPIEEE64, 0x3FE0000000000000 // 0.5 // user: %4 + %2 = float_literal $Builtin.FPIEEE64, 0x4000000000000000 // 2 // user: %4 %3 = struct_extract %0 : $Double, #Double._value // user: %4 - %4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 + %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %5 = struct $Double (%4 : $Builtin.FPIEEE64) // user: %6 return %5 : $Double // id: %6 -} // end sil function '$s7sample34testyS2dF' +} // end sil function '$s7sample44testyS2dF'%4 = builtin "fmul_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5 %4 = builtin "fdiv_FPIEEE64"(%3 : $Builtin.FPIEEE64, %2 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %5割り算と掛け算がちゃんと別れています。
/ 2
を自動的に* 0.5
に変換される、なんてことはないようです。
ちなみに、test(:)
に@inlinable
を追加したら、検証1と同じ計算した結果の100の値が保持されました。展開されて、最適化が施されたようです。まとめ
- リテラル値同士の計算は 最適化されて計算した結果のみ保持する
これを色々調べて思ったのは、ただ早いから
* 0.5
を選ぶのは軽率かなと思いました。可読性の問題もあったり、上の最適化されて結果同じ場合もあったりするので、そこらへんを正しく見極めて書いていくことが大事だと感じました。次は16日目、@keitatata による redis レプリケーションとシャーディング です。お楽しみに。
参考資料
以下の資料は今回の記事を書く上で非常に参考になりました。ありがとうございます。
http://llvm.org/devmtg/2015-10/slides/GroffLattner-SILHighLevelIR.pdf
https://blog.waft.me/2018/01/09/swift-sil-1/
https://github.com/apple/swift/blob/master/docs/SIL.rst
https://qiita.com/Kuniwak/items/cbf6b88db249838895b5