しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

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です。

github.com

pkg-configとはライブラリを使う際に必要な情報を共通したインターフェースで提供してくれるツールです。
今回のエラーではpkg-configを使ってNokogiriのビルドに必要な情報を取得しようとしたんだと思います。

pkg-configとは、ライブラリを利用する際に必要となる各種フラグやパス等を、共通したインターフェースで提供でするための手段である。
pkg-configは、環境変数PKG_CONFIG_PATHのパスに存在する *.pc ファイルに記録された情報を元に、ビルドの際に必要な文字列を返す。

pkg-config - Wikipediaより

今回の件では下のようなメッセージがあったので、おそらく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が平成の年数になったりして期待した動きをしない事があります。

常に西暦の年数を取りたい場合は下のような実装をします。
もし毎回初期化する事でパフォーマンスが悪い場合は

Calendar(identifier: .gregorian).component(.year, from: Date())

まとめ

Dateの日付のみを扱う方法について調べましたが、もし実装するとしたらIntで保持するのが良さそうです。
処理は冗長になりがちなので、その辺りをうまく共通化するともっと便利になりそうです。

DOFavoriteButtonでアニメーション付きお気に入りボタンを実装する

DOFavoriteButtonというアニメーション付きのボタンを実装できるライブラリを試してみました。

github.com

f:id:llcc:20180603191058g:plain

DOFavoriteButtonのインストール

CocoaPodsでインストールしました。
本家はSwift4に対応してないようなので、fumiyasacさんのSwift4対応フォークリポジトリを利用させていただきました。

target 'MyApp' do
  use_frameworks!

  pod 'DOFavoriteButton', git: 'git@github.com:fumiyasac/DOFavoriteButton.git'
end

DOFavoriteButtonの使い方

まずはアイコンの画像をプロジェクトに追加します。

f:id:llcc:20180603191740p:plain

次は画面上にUIButtonを配置してクラスをDOFavoriteButtonにします。

f:id:llcc:20180603192040p:plain

f:id:llcc:20180603192050p:plain

クラスをDOFavoriteButtonにしたら選択時の色やアニメーションの円の色などを設定します。

f:id:llcc:20180603192356p:plain

最後にボタンタップ時の挙動を追加します。

import UIKit
import DOFavoriteButton

class ViewController: UIViewController {
    @IBAction func tapBtn(_ sender: DOFavoriteButton) {
        if sender.isSelected {
            sender.deselect()
        } else {
            sender.select()
        }
    }
}

これでボタンがアニメーション付きでON/OFF切り替わるようになりました。

f:id:llcc:20180603192715p:plain

【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は下のような呼ばれ方がしている事が分かりました。

f:id:llcc:20180603190013p:plain

まとめ

最初は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; }
    }
}

UnityのUpdateとFixedUpdateの違いを確認する

Unityのフレーム毎に呼ばれる関数はUpdateとFixedUpdateがあります。
今回はこの2つの違いを確かめてみます。

UpdateとFixedUpdateの違い

2つはFixedUpdateは1秒間あたりの呼び出し回数が固定、Updateは端末の状態によって変わることがあるという違いがあります。
Updateは1フレーム毎に呼び出されるので、端末の描画速度によって変化するのです。

UpdateとFixedUpdateの違いの確認

まずは2つのメソッドが呼ばれることを確認します。
下のようにログ出力処理を追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyScript : MonoBehaviour {
    private void Update () 
    {
        Debug.Log("Update");
    }

    private void FixedUpdate()
    {
        Debug.Log("FixedUpdate");
    }
}

コンソールを見るとログが出ている事が分かります。
下の方のログを見るとUpdateとFixedUpdateの呼ばれるタイミングが一定ではない事が分かります。

f:id:llcc:20180602180345p:plain

FixedUpdateの呼ばれるタイミングはEdit → Project Settings → Timeで設定する事ができます。
Fixed Timestepを変えればその秒数毎にFixedUpdateが呼ばれるようになります。

f:id:llcc:20180602181404p:plain

実際にFixed Timestepを1秒に変更したところ、FixedUpdateは1秒に1回だけ呼ばれるようになりました。

f:id:llcc:20180602181636p:plain

FixedUpdateはTime.timeScaleにも影響を受けます。
例えば下のようにtimeScaleを0にすればFixedUpdateは呼ばれなくなります。
もしtimeScaleを0.1にした場合はFixedUpdateはtimeScale=1の10分の1の頻度で呼ばれるようになります。

public class MyScript : MonoBehaviour {
    private void Start()
    {
        Time.timeScale = 0;
    }

    private void Update ()
    {
        Debug.Log("Update");
    }

    private void FixedUpdate()
    {
        Debug.Log("FixedUpdate");
    }
}

UpdateとFixedUpdateの使い分け

FixedUpdateは時間のカウントアップなど、一定のタイミングで呼ばれてほしい処理を記述するのがオススメです。
一方でUpdateはInput関連をセットするのがオススメです。
というのもInputは入力したフレームでしか有効にならないのでFixedUpdateではフレームとのタイミングがずれて入力を検知できない事があります。
そのため、Inputはフレーム毎に呼ばれるUpdateに書く必要があります。

まとめ

UpdateとFixedUpdateなどは微妙な違いなので再現性の低い不具合などに繋がりそうだと感じました。
今後はこの辺りもしっかり意識しつつ開発をしていきたいと思います。

LICEcap for Macで画面が真っ黒になるのでGIPHYに乗り換えた

LICEcapで作られるGifが下のように真っ黒になる現象に遭遇しました。

f:id:llcc:20180529133550p:plain

再インストールしても直らないのでGIPHYというアプリに乗り換えました。

GIPHY Capture. The GIF Maker

GIPHY Capture. The GIF Maker

  • Giphy, Inc.
  • Video
  • Free

GIPHYとは

GIPHYはおもしろGifをアップロードするサービスです。
Macのクライアントでは画面キャプチャをアニメーションGifとして保存したりGIPHYにアップロードする機能があります。

giphy.com

GIPHYの使い方

GIPHYを立ち上げると下のような画面になります。

f:id:llcc:20180529134010p:plain

下の録画ボタンを押せば録画開始です。
最大で30秒まで録画することができます。

f:id:llcc:20180529134048p:plain

録画完了すると画面下部にサムネイルが表示されます。

f:id:llcc:20180529134224p:plain

サムネイルを押すと下のようなプレビュー画面に遷移します。

f:id:llcc:20180529134259p:plain

プレビューのSave asボタンを押せばGifをローカルに保存することができます。

f:id:llcc:20180529134325p:plain

まとめ

GIPHYはシンプルだしデザインも今風でとても良い感じでした。
現状不便さもないので継続して使ってみようと思います。