しめ鯖日記

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

はてなブログのSSL対応でカスタムシェアボタンが動かなくなった時の対策

タイトルの通り、はてなブログのSSL化でカスタムシェアボタンが動かなくなったので対策を書いていきます。
利用させて頂いたカスタムシェアボタンは下サイトのものです。

www.yukihy.com

SSL化で発生した問題

下のようにはてブ数とFacebookのシェア数がずっとローディングのままになりました。

f:id:llcc:20180627150028p:plain

対応方法

1つ目の修正点は下のタグです。
http://code.jquery.com/jquery-1.9.1.min.jsのhttp://となっている箇所をhttps://に変更しました。

<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>

2つ目の修正箇所はJavascriptの下処理です。
ここのhttp://api.b.st-hatena.com/entry.count?callback=?となっている部分をhttps://b.hatena.ne.jp/entry.count?callback=?に変更しました。

jQuery.ajax({
  url:'http://api.b.st-hatena.com/entry.count?callback=?',
  dataType:'jsonp',
  data:{
    url:url
  },
  success:function(res){
    jQuery( selcter ).text( res || 0 );
  },
  error:function(){
    jQuery( selcter ).text('0');
  }
});

以上で対応完了です。
無事にはてブ数とシェア数が取れるようになりました。

f:id:llcc:20180627150507p:plain

【アプリ開発】AppLaunchPadでアプリのスクショを作ってみる

AppLaunchPadというWEBサービスを試してみました。

theapplaunchpad.com

これはAppStoreやGooglePlayに掲載するおしゃれなスクリーンショットを簡単に作れるものです。
有料限定のテンプレートも多いのですが、下のような画面を簡単に作る事ができます。

f:id:llcc:20180626140104p:plain

AppLaunchPadの料金体系(2018/6 時点)

無料で使うことができます。
有料プランになればテンプレートの数が増えるなどの多くのメリットがあります。

f:id:llcc:20180626141023p:plain

theapplaunchpad.com

AppLaunchPadの使い方

Dashboardに飛ぶとまずはプラットフォームを選択します。

f:id:llcc:20180626141208p:plain

続けてテンプレートを選択します。
無料プランなので右上の3つのみ選ぶ事ができます。

f:id:llcc:20180626141229p:plain

次はデバイスを選択します。
無料プランでは上の5つが選択できます。

f:id:llcc:20180626141308p:plain

最後にスクリーンショットをアップロードします。

f:id:llcc:20180626141357p:plain

画像をアップロードするとスクリーンショットが作られます。

f:id:llcc:20180626141606p:plain

画面下部から背景色などを変更する事ができます。

f:id:llcc:20180626141650p:plain

Exportすると下のように様々なサイズの画像を取得できます。

f:id:llcc:20180626141903p:plain

ただしiPadは有料プラン限定なので下のように横帯が入ります。

f:id:llcc:20180626141955p:plain

まとめ

手軽にきれいなスクリーンショットを作れるので、スクリーンショット制作で苦戦している場合は使ってみるのも良いかと思います。
自分の場合はスクリーンショットを細かくカスタマイズしたいのでScreenshotMakerで作っていこうと思います。

www.cl9.info

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:)は呼ばれていないので、実装の際は注意する必要があります。

f:id:llcc:20180628141025p:plain

下へスクロールすると下のようなログが出力されました。
tableView(_:prefetchRowsAt:)が呼ばれるのとセットでtableView(_:prefetchRowsAt:)が呼ばれています。

f:id:llcc:20180628141143p:plain

上にスクロールすると下のようなログが出ました。
rowが13~15のtableView(_:prefetchRowsAt:)が再度呼ばれていました。

f:id:llcc:20180628141435p:plain

まとめ

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

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が平成の年数になったりして期待した動きをしない事があります。

常に西暦の年数を取りたい場合は下のような実装をします。
もし毎回初期化する事でパフォーマンスが悪い場合はstatic変数としてCalendarを保持するのもいいかもしれません。

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を返す原因について探っていたんですが、動画のドロップフレームなど新しい事を学べて面白かったです。
この辺りはいざ不具合にぶつかった時にハマりやすいので今後も地道に勉強していこうと思います。