【アプリ開発】AppLaunchPadでアプリのスクショを作ってみる
AppLaunchPadというWEBサービスを試してみました。
これはAppStoreやGooglePlayに掲載するおしゃれなスクリーンショットを簡単に作れるものです。
有料限定のテンプレートも多いのですが、下のような画面を簡単に作る事ができます。
AppLaunchPadの料金体系(2018/6 時点)
無料で使うことができます。
有料プランになればテンプレートの数が増えるなどの多くのメリットがあります。
AppLaunchPadの使い方
Dashboardに飛ぶとまずはプラットフォームを選択します。
続けてテンプレートを選択します。
無料プランなので右上の3つのみ選ぶ事ができます。
次はデバイスを選択します。
無料プランでは上の5つが選択できます。
最後にスクリーンショットをアップロードします。
画像をアップロードするとスクリーンショットが作られます。
画面下部から背景色などを変更する事ができます。
Exportすると下のように様々なサイズの画像を取得できます。
ただしiPadは有料プラン限定なので下のように横帯が入ります。
まとめ
手軽にきれいなスクリーンショットを作れるので、スクリーンショット制作で苦戦している場合は使ってみるのも良いかと思います。
自分の場合はスクリーンショットを細かくカスタマイズしたいのでScreenshotMakerで作っていこうと思います。
UITableViewのprefetchDataSourceについて調べてみる
iOS10からUITableViewのprefetchDataSourceというプロパティーが追加されてたので調べてみました。
prefetchDataSourceとは
prefetchDataSource
はUITableViewの高速化に使うプロパティーです。
下のようにUITableViewDataSourcePrefetching
に準拠したオブジェクトをセットして使います。
class MasterViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.prefetchDataSource = self } } extension MasterViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { } }
tableView(_:prefetchRowsAt:)
はtableView(_:cellForRowAt:)
の前に呼ばれるので、サーバーからのデータ取得など時間のかかる処理を実施するのに適しています。
tableView(_:prefetchRowsAt:)の呼ばれるタイミング
実際にどのタイミングでtableView(_:prefetchRowsAt:)
が呼ばれるかも調べてみました。
調査は下のようにログをしかけて実施しました。
import UIKit class MasterViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.prefetchDataSource = self } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 100 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = "\(indexPath)" print(#function, indexPath) return cell } } extension MasterViewController: UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { print(#function, indexPaths) } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { print(#function) } }
アプリ起動時は下のようなログが出力されました。
tableView(_:prefetchRowsAt:)
はrowが13~22のCellのものが呼び出されている事が分かります。
rowが0~12のtableView(_:prefetchRowsAt:)
は呼ばれていないので、実装の際は注意する必要があります。
下へスクロールすると下のようなログが出力されました。
tableView(_:prefetchRowsAt:)
が呼ばれるのとセットでtableView(_:prefetchRowsAt:)
が呼ばれています。
上にスクロールすると下のようなログが出ました。
rowが13~15のtableView(_:prefetchRowsAt:)
が再度呼ばれていました。
まとめ
prefetchDataSource挙動に癖がありますが、うまく使えば便利そうだと感じました。
最近修正したアプリで「Cellの中でサーバーを画像を取得する」って事をやっていたので、そこで試してみようと思います。
NokogiriのインストールでERROR: cannot discover where libxml2 is located on your system. please make sure `pkg-config` is installed.というエラーが出た時の対策
Nokogiriをインストール中に表題のエラーが出た時の対処法です。
メッセージは下の通りです。
current directory: /Users/xxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/nokogiri-1.8.3/ext/nokogiri /Users/xxxxxx/.rbenv/versions/2.5.1/bin/ruby -r ./siteconf20180619-62442-1vrm1wp.rb extconf.rb --use-system-libraries checking if the C compiler accepts ... yes checking if the C compiler accepts -Wno-error=unused-command-line-argument-hard-error-in-future... no Building nokogiri using system libraries. pkg-config could not be used to find libxml-2.0 Please install either `pkg-config` or the pkg-config gem per gem install pkg-config -v "~> 1.1" pkg-config could not be used to find libxslt Please install either `pkg-config` or the pkg-config gem per gem install pkg-config -v "~> 1.1" pkg-config could not be used to find libexslt Please install either `pkg-config` or the pkg-config gem per gem install pkg-config -v "~> 1.1" ERROR: cannot discover where libxml2 is located on your system. please make sure `pkg-config` is installed. *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. Provided configuration options: --with-opt-dir --without-opt-dir --with-opt-include --without-opt-include=${opt-dir}/include --with-opt-lib --without-opt-lib=${opt-dir}/lib --with-make-prog --without-make-prog --srcdir=. --curdir --ruby=/Users/xxxxxx/.rbenv/versions/2.5.1/bin/$(RUBY_BASE_NAME) --help --clean --use-system-libraries --with-zlib-dir --without-zlib-dir --with-zlib-include --without-zlib-include=${zlib-dir}/include --with-zlib-lib --without-zlib-lib=${zlib-dir}/lib --with-xml2-dir --without-xml2-dir --with-xml2-include --without-xml2-include=${xml2-dir}/include --with-xml2-lib --without-xml2-lib=${xml2-dir}/lib --with-libxml-2.0-config --without-libxml-2.0-config --with-pkg-config --without-pkg-config --with-xslt-dir --without-xslt-dir --with-xslt-include --without-xslt-include=${xslt-dir}/include --with-xslt-lib --without-xslt-lib=${xslt-dir}/lib --with-libxslt-config --without-libxslt-config --with-exslt-dir --without-exslt-dir --with-exslt-include --without-exslt-include=${exslt-dir}/include --with-exslt-lib --without-exslt-lib=${exslt-dir}/lib --with-libexslt-config --without-libexslt-config To see why this extension failed to compile, please check the mkmf.log which can be found here: /Users/xxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nokogiri-1.8.3/mkmf.log extconf failed, exit code 1 Gem files will remain installed in /Users/xxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/nokogiri-1.8.3 for inspection. Results logged to /Users/xxxxxx/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nokogiri-1.8.3/gem_make.out An error occurred while installing nokogiri (1.8.3), and Bundler cannot continue. Make sure that `gem install nokogiri -v '1.8.3' --source 'https://rubygems.org/'` succeeds before bundling.
メッセージに記載の通り、pkg-configを入れたら解決しました。
gem install pkg-config
pkg-configとは何か
解決はしたのですが、気持ち悪いのでもう少し深掘りしてみます。
まずは下のエラーメッセージに出てきたpkg-configについて調べます。
ERROR: cannot discover where libxml2 is located on your system. please make sure `pkg-config` is installed.
pkg-configのGemはpkg-configというライブラリをRubyで実装したものになります。
リポジトリは下URLです。
pkg-configとはライブラリを使う際に必要な情報を共通したインターフェースで提供してくれるツールです。
今回のエラーではpkg-configを使ってNokogiriのビルドに必要な情報を取得しようとしたんだと思います。
pkg-configとは、ライブラリを利用する際に必要となる各種フラグやパス等を、共通したインターフェースで提供でするための手段である。
pkg-configは、環境変数PKG_CONFIG_PATHのパスに存在する *.pc ファイルに記録された情報を元に、ビルドの際に必要な文字列を返す。
今回の件では下のようなメッセージがあったので、おそらくpkg-configを使ってlibxml2のパスを探そうとしたけどpkg-configがないからエラーになったという事だと思います。
pkg-config could not be used to find libxml-2.0
libxml2とは何か
これは名前の通りXMLを操作するライブラリです。
末尾に付いている2はlibxml1の改良版だからです。
元々libxml1があったんですが、色々な問題があってlibxml2を作ったようです。
詳しい使い方などはまた機会あれば調べてみようと思います。
The XML C parser and toolkit of Gnome
まとめ
時々名前を聞くpkg-configですが、詳細が分かってすっきりしました。
今後もエラーなどに遭遇したら少しでも詳しく調べるようにしたいと思います。
【Swift】Dateの日付のみを扱う方法を考える
iOSアプリ開発ではDate型があるのですが、これは日付型のみを扱う事ができません。
そのため「RealmなどのDBから特定日付のレコードを取り出したい」という時に開始日と終了日を指定する必要があって結構めんどくさいです。
class History: Object { @objc dynamic var id = 0 @objc dynamic var date = Date() } let realm = try! Realm() // startDateとendDateで比較するのでめんどくさい let history = realm.objects(History.self).filter("date >= %@ && date < %@", startDate, endDate).first
「dateカラムには常にその日の0:00のデータを入れるというルールにする」って方針もあるのですが、これだと別の国に移動してタイムゾーンがずれた時などにデータがおかしくなる可能性があります。
let history = History() if let date = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()).date { history.date = date }
今回はそういった問題の対策として、Dateを文字列として扱う事で時間情報を省けないかを検討してみます。
Dateを文字列として保存する実装
上で挙げたHistoryオブジェクトのdateカラムを文字列化する場合、下のような実装ができるかと思います。
dateのゲッターとセッターを提供して、その内部ではdateStrカラムからのデータ取得・データの保存を行います。
class History: Object { @objc dynamic var id = 0 @objc dynamic var dateStr = "20180101" var date: Date? { get { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" return formatter.date(from: dateStr) } set { if let date = newValue { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" dateStr = formatter.string(from: date) } } } }
日付のセットは下のように行います。
let history = History() history.date = Date() print(history.date)
特定の日付のレコードの取得は下のように行います。
let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" print(try? Realm().objects(History.self).filter("dateStr = %@", formatter.string(from: Date())).first)
RealmではStringで比較演算子を使う事ができないので、下のように比較する必要があります。
let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { print(try? Realm().objects(History.self).filter { $0.dateStr >= formatter.string(from: startDate) && $0.dateStr < formatter.string(from: endDate) }.count) }
ここまでで取得・保存の実装を見てきましたが少し冗長な書き方になってしまいそうです。
実装のシンプルさという面ではDateを文字列として扱うのはあまり良くないかもしれません。
Dateを文字列として扱う場合のパフォーマンス
次に文字列として扱った場合のパフォーマンスを見てみようと思います。
最初にdateのセットを見ていきます。
下のように100回セットした時の時間を計測します。
端末はiPhone5を利用しました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 let history = History() dates.forEach { date in history.date = date } print(Date().timeIntervalSince1970 - t)
結果は下のとおりです。
100回で0.1秒なので負荷を気にしなくて良いレベルかと思います。
0.108951091766357 // 1回目 0.0738258361816406 // 2回目 0.066342830657959 // 3回目
次は日付の取得を見ていきます。
let histories: [History] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 let history = History() history.date = component.date return history } let t = Date().timeIntervalSince1970 let history = History() histories.forEach { history in _ = history.date } print(Date().timeIntervalSince1970 - t)
結果は下の通りです。
こちらもセットとほぼ同じくらいの速度でした。
0.078549861907959 0.0887308120727539 0.0748848915100098
次にRealmでの検索を見ていきます。
レコードは事前に10000件登録してあります。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" let realm = try? Realm() _ = realm?.objects(History.self).filter("dateStr = %@", formatter.string(from: date)) } print(Date().timeIntervalSince1970 - t)
結果は下の通りです。
日付のセットより少し重いですが気にするほどではなさそうです。
0.303496122360229 0.253895998001099 0.246631860733032
最後に複数データの取得を試してみました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: date) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: date) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { let realm = try? Realm() _ = realm?.objects(History.self).filter { $0.dateStr >= formatter.string(from: startDate) && $0.dateStr <= formatter.string(from: endDate) }.count } } print(Date().timeIntervalSince1970 - t)
これは全レコードを毎回インスタンス化するという事もあって非常に遅かったです。
パフォーメンスの面で言うと文字列でDateを扱うのは難しそうです。
96.0780339241028 92.7221720218658 99.2258989810944
DateをIntとして保存する実装
Dateを文字列で扱うのは、複数データ取得時のパフォーマンス面で厳しそうでした。
代わりにIntとして扱う実装を試してみます。
Intとして扱う場合、Historyテーブルは下のような形の実装が考えられます。
class History: Object { @objc dynamic var id = 0 @objc dynamic var dateValue = 20180101 var date: Date? { get { let year = dateValue / 10000 let month = (dateValue % 10000) / 100 let day = (dateValue % 100) let dateComponent = DateComponents(calendar: Calendar.current, year: year, month: month, day: day) return dateComponent.date } set { if let date = newValue { let year = Calendar.current.component(.year, from: date) let month = Calendar.current.component(.month, from: date) let day = Calendar.current.component(.day, from: date) dateValue = year * 10000 + month * 100 + day } } } }
文字列の時にパフォーマンスが悪かった「複数データ取得」の測定をしてみました。
let dates: [Date] = (0...100).compactMap { var component = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) component.day = (component.day ?? 0) - $0 return component.date } let t = Date().timeIntervalSince1970 dates.forEach { date in var startDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) startDateComponent.day = (startDateComponent.day ?? 0) - 10 let endDateComponent = Calendar.current.dateComponents([.calendar, .year, .month, .day], from: Date()) if let startDate = startDateComponent.date, let endDate = endDateComponent.date { let startDateValue = Calendar.current.component(.year, from: startDate) * 10000 + Calendar.current.component(.month, from: startDate) * 100 + Calendar.current.component(.day, from: startDate) let endDateValue = Calendar.current.component(.year, from: endDate) * 10000 + Calendar.current.component(.month, from: endDate) * 100 + Calendar.current.component(.day, from: endDate) let realm = try? Realm() _ = realm?.objects(History.self).filter("dateValue >= %d and dateValue <= %d", startDateValue, endDateValue).count } } print(Date().timeIntervalSince1970 - t)
測定結果は下の通りです。
この速度なら採用しても問題なさそうです。
0.374364852905273 0.377358913421631 0.369530916213989
実装においての注意
この実装ですが、和暦の対策が必要です。
和暦対策をしないと、ユーザーの端末設定によってはyearが平成の年数になったりして期待した動きをしない事があります。
常に西暦の年数を取りたい場合は下のような実装をします。
もし毎回初期化する事でパフォーマンスが悪い場合はstatic変数としてCalendarを保持するのもいいかもしれません。
Calendar(identifier: .gregorian).component(.year, from: Date())
まとめ
Dateの日付のみを扱う方法について調べましたが、もし実装するとしたらIntで保持するのが良さそうです。
処理は冗長になりがちなので、その辺りをうまく共通化するともっと便利になりそうです。
DOFavoriteButtonでアニメーション付きお気に入りボタンを実装する
DOFavoriteButtonというアニメーション付きのボタンを実装できるライブラリを試してみました。
DOFavoriteButtonのインストール
CocoaPodsでインストールしました。
本家はSwift4に対応してないようなので、fumiyasacさんのSwift4対応フォークリポジトリを利用させていただきました。
target 'MyApp' do use_frameworks! pod 'DOFavoriteButton', git: 'git@github.com:fumiyasac/DOFavoriteButton.git' end
DOFavoriteButtonの使い方
まずはアイコンの画像をプロジェクトに追加します。
次は画面上にUIButtonを配置してクラスをDOFavoriteButtonにします。
クラスをDOFavoriteButtonにしたら選択時の色やアニメーションの円の色などを設定します。
最後にボタンタップ時の挙動を追加します。
import UIKit import DOFavoriteButton class ViewController: UIViewController { @IBAction func tapBtn(_ sender: DOFavoriteButton) { if sender.isSelected { sender.deselect() } else { sender.select() } } }
これでボタンがアニメーション付きでON/OFF切り替わるようになりました。
【iOS】CMSampleBufferGetImageBufferがnilを返す時の対処法
AVFoundationを使うと端末のカメラで撮っている映像をUIImageとして取得する事ができます。
その際にCMSampleBufferGetImageBufferというメソッドを使うんですが、これがnilを返した時の対処法です。
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) // → これがnilになる }
原因ですがメソッド名間違いでした。
didDropのメソッドではなくdidOutputを使う事で無事にUIImageを取得する事ができました。
// 間違い func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { } // 正解 func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { }
didDropとdidOutputの違い
簡単に言うとdidDropはフレームをドロップした時に呼ばれるメソッドで、CMSampleBufferには映像データが入っていません。
そのため映像データが欲しい場合はdidOutputを使う必要があります。
フレームをドロップするとはどういうことか
フレームのドロップとは映像の時間の帳尻を合わせる為のものです。
動画は想定していた1分間辺りフレーム数と実際のフレーム数がずれる事があります。
フレーム数がずれてしまうと10分の映像だったつもりが実際には少し長くなったりしてしまったり、困った事態が起こります。
その対策として、帳尻を合わせるためにフレームをスキップしたりします。
これがフレームをドロップするということです。
詳しい挙動を調べるため、下のようにdidDropとdidOutputの呼び出しタイミングを調べてみました。
func captureOutput(_ output: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { print(#function) } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { print(#function) }
調べたところ、didDropとdidOutputは下のような呼ばれ方がしている事が分かりました。
まとめ
最初はCMSampleBufferGetImageBufferがnilを返す原因について探っていたんですが、動画のドロップフレームなど新しい事を学べて面白かったです。
この辺りはいざ不具合にぶつかった時にハマりやすいので今後も地道に勉強していこうと思います。
C#のプロパティーの書き方
基本的なところですがC#のプロパティーの書き方について調べました。
プロパティーは下のように2種類の書き方があります。
class MyClass { public int myProperty1 = 0; public int myProperty2 { get { return 0; } set { int myValue = value; } } }
それぞれ下のように呼び出します。
MyClass c = new MyClass(); Debug.Log(c.myProperty1); // → 0 Debug.Log(c.myProperty2); // → 0 c.myProperty1 = 2; Debug.Log(c.myProperty1); // → 2
プロパティーはセッターだけprivateにするといった事も可能です。
class MyClass { public int myProperty1 = 0; public int myProperty2 { get { return 0; } private set { int myValue = value; } } }