IBAction の設定で少しはまったこと

iOS 開発で storyboard や xib を使ってレイアウトを組むことは一般的です(まぁ複雑なレイアウトを組むときは除いて)。自分も基本的にはこれらを使ってレイアウトを組んでいます。仕事で iOS 開発をしていたとき、カスタムセルを作る必要があったので、いつも通りに組んで IBAction の処理を書いて〜 動作確認してみたら、IBAction の処理に入ってこない、イベントが拾えていない。なんでだということで少しばかり時間を割いてしまいました。結局解決はしたのですが、「あっ、そんなのあったんだ」という気付きがあったのでまとめておきます。

カスタムセルを作って、そこにボタンを配置してクリックイベントを拾うということはよくある処理だと思います。以下の図のように Connection を Action にしで Event を設定します。

f:id:tunanosuke:20140621011019p:plain

設定をし Connect すると Button に紐付くメソッドができるのであとはそのメソッドに処理を書いていけば、設定した Event が行われたときにメソッドが呼ばれ処理が実行されます。ただ今回まったく処理が実行されませんでした。理由は User Interaction Enabled にチェックが入っていなかったことが原因でした。

  f:id:tunanosuke:20140621011626p:plain

デフォルトではチェックが入っているためこれまで気にしていませんでした。ここのチャックが外れているとイベントを拾おうとしてもまったく拾えなくなります。今回でいうと、カスタムセル自体がイベントは受け付けませんよーという状態になっていました。チェックを入れることで無事解決しました。

思わぬところで時間を割いてしまいましたが、新たな気付きもできました。iOS8 対応とか swift 移行とかでこれから秋くらいにかけては iOS に触れる機会が増えそうなので、注意しなければと思いました。

 

casperjs で iTunesConnect からアプリのダウンロード数をスクレイピングしてみた

以前、 Sales and Trends からダウンロード数を出力というタイトルで iTunesConnect からアプリのダウンロード数を取得する方法について書きました。Apple 謹製のスクリプトを使っているのですが、フィルタリングが弱く柔軟に好きな情報を取得できない。何か別の方法ないか考えていたとき、casperjs でもしかしたらできるんじゃないかと思いやってみた。casperjs は主に Web のテストとかで使われるケースが多いけど、Web の情報を簡単にスクレイプできるのでかなり便利です。

casperjs とは

casperjs は Javascript で実装された PhantomJS のラッパーです。PhanomJS とはコンソール上から webkit ブラウザを操作する仕組みでクライアントサイドのテストフレームワークとして利用されています。webkit ブラウザなので chrome とか firefox とかですね。コンソール上から javascript を実行したり Cookie 書き換えたり UgerArgent 書き換えたりと色々できる。casperjs はそのラッパーなので PhantomJS にプラスして、テストやスクレイピングするための便利な機能が多く含まれています。そしてなにしろ可読性も高く、ここをクリックしたらこうなって次ここをクリックしたらこの情報を取得してという流れが分かりやすく書くことができる便利なやつです。

スクレイピング

実際にスクレイピングしてみます。コードはこんな感じです。

var user = 0;
var units = [];
var casper = require('casper').create();

casper.start('https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/', function() {
this.echo(this.getTitle());
});

casper.then(function() {
this.evaluate(function(username, password) {
document.querySelector('#accountname').value = username;
document.querySelector('#accountpassword').value = password;
document.querySelector('.sign-in').click();
}, 'ID', 'PW');
});

casper.thenOpen('https://reportingitc2.apple.com/', function() {
this.clickLabel('対象アプリ名', 'span');
});

casper.then(function() {
this.wait(3000, function() {
this.clickLabel('Lifetime', 'a');
this.wait(3000, function() {
var elements = this.getElementsInfo('#exportContainer table tbody tr td');
var j = 0;
for (i in elements) {
if (i > 1) {
units[j] = elements[i]['text'];
j++;
}
}
});
});
});

casper.then(function() {
for (i in units) {
user += Number(units[i]);
}
user = user.toString().replace(/(d)(?=(ddd)+$)/g , '$1,');
require('fs').write('units.txt', user)
});

casper.run(); 

これを実行すればアプリのダウンロード数を取得することができます。ただ要素の指定や一部の処理は公開しているアプリの数などでかわってくるかもしれないです。

先に言いましたが、casperjs は可読性が高いのでだいたい何してるか想像つくと思いますが、簡単に処理を追ってみます。

1. casper.start で URL とコールバック関数を定義しています。URL は iTunes Connect のログインページ、コールバックでは getTitleメソッドでページのタイトルを取得しコンソールに表示させています。 

2. ログイン画面に遷移させた後、casper.then の evaluate でログインフォームに ID と PW を入力します。evaluate は DOM 要素に値を挿入させたいときなどに利用します。ここでは DOM のid要素を指定してID、PWを挿入しています。

3. ここまでで iTunes Connect へのログインが完了しましたので、Sales and Trends にアクセスし対象のサービスの情報を表示させるよう処理させます。thenOpen で Sales and Trendsを開き、clickLabel  が第一引数に指定した文字列をページ上から探し、データを取得したいアプリの情報に絞っています。

4. ここから実際にデータを取得する処理になります。まず Lifetime のリンクをクリック、対象アプリの全ダウンロード状況を表示させます。wait で3秒間を置いているのは画面の読み込みが走るので念のためという感じです。読み込み完了後、getElementInfo でダウンロード数を取得し配列にいれます。配列なのは Lifetime クリックした際に年ごとにダウンロード数が表示されるので、それぞれの年のデータを配列にいれています。

5. これでデータが取得できました。最後に配列の中身の合計を出し、3桁区切りにしてファイルに出力させてます。

このスクリプトを定期実行させれば自動でデータが取得できるし、Web 上に表示させておけば誰もが数値を見る事ができますね。チームで開発していて、目標値を掲げている場合にはこのような見える化はモチベーションの一つにもなるのではと思います。拡張していけば色んなパターンのデータを自動で集計できそうです。

デメリット

このやり方は DOM 要素の構成に依存するため、iTunes Connest の UI 変更などがあるとスクリプトも変更しなければなりません。ただ指定要素などが変わってくるくらいなのでさほど面倒ではないかなと思っています。頻繁に UI 変更があるわけでもないですし。

 

以上、casperjs でアプリのダウンロード数をスクレイピングする方法についての紹介でした。

ちなみにcasperjs は Web の結合テストをおこなえる便利機能が揃っています。selenium と違い導入コストも低いですし、Web の結合テストを簡単に実行できるので、取りかかりにはおすすめです。

 

vagrant + java + tomcat

ローカル環境に vagrant で仮想環境を作り、tomcat 上で java アプリケーションを動かしてみる。今の業務での開発はローカルで開発→検証環境へアップ→リリースという流れが主です。ここで、”ローカルで開発→検証環境へアップ”という部分で、そもそもローカルでも検証環境と同等の状態で開発できたら楽だし手戻り減ります。(ここで手戻りと言ったのは、ほんとごく稀に検証上げたらこの機能が動かない、文字化けしたというケースを聞いたり見たりすることがあるためです)ローカルと検証を同等の環境にし開発を進めるための一歩として、vagrant を使って環境を構築してみます。vagrant については以前記載していますのでそちらを参照ください。

jdk のインストール

以降の環境はすべて仮想環境内での操作になります。まずは java の実行環境を作るために jdk をインストールします。

sudo yum list | grep java
sudo su install java-1.7.0-openjdk.x86_64 java-1.7.0-openjdk-devel.x86_64

ここでは jdk のリストを確認し、インストールを実行しています。完了したら正しくインストールされているか確認しておきます。

java -version

"java version "1.7.0_45""のように表示されればOK。ちなみにJavaは "/usr/lib/jvm/java" にインストールされていますので確認してみてください。

tomcat のインストール

 続いて tomcat のインストールです。  tomcat はダウンロードしそれを解凍します。ダウンロードする際は wget を利用しますが、もしコマンドが使えなければ以下を実行しインストールしておきます。

sudo yum -y install wget

ダウンロードと解凍。

wget http://ftp.jaist.ac.jp/pub/apache/tomcat/tomcat-7/v7.0.53/bin/apache-tomcat-7.0.53.tar.gzomcat-7.0.50.tar.gz
tar -xzvf ~/apache-tomcat-7.0.53.tar.gz
sudo cp -rf apache-tomcat-7.0.53 /usr/local/tomcat

tomcat ユーザを作成し、権限を変更しておきます。

sudo useradd -s /sbin/nologin tomcat
sudo chown tomcat:tomcat -R /usr/local/tomcat/

tomcat のインストールは完了です。

javatomcatのインストールが完了したところで環境変数にそれぞれの設定を追加しておきましょう。tomcat.sh に以下のように記載。

sudo vi /etc/profile.d/tomcat.sh



export JAVA_HOME=/usr/lib/jvm/java

export PATH=$PATH:$JAVA_HOME/bin

export CLASSPATH=.:$JAVA_HOME/jre/lib:$JAVA_HOME/lib:$JAVA_HOME/lib/tools.jar

export TOMCAT_HOME=/usr/local/tomcat

export CATALINA_HOME=/usr/local/tomcat

export CLASSPATH=$CLASSPATH:$CATALINA_HOME/common/lib

tomcat の起動スクリプト

最後に起動スクリプトを書けば完了です。

sudo vi /etc/rc.d/init.d/tomcat



#!/bin/bash

#

# Startup script for the tomcat

#

# chkconfig: 345 80 15

# description: Tomcat is a Servlet+JSP Engine.



# Source function library.

. /etc/rc.d/init.d/functions

source /etc/profile.d/tomcat.sh



start(){

    if [ -z $(/sbin/pidof java) ]; then

        echo "Starting tomcat"

        /usr/local/tomcat/bin/startup.sh

        touch /var/lock/subsys/tomcat

    else

        echo "tomcat allready running"

    fi

}



stop(){

    if [ ! -z $(/sbin/pidof java) ]; then

        echo "Shutting down tomcat"

        /usr/local/tomcat/bin/shutdown.sh

        until [ -z $(/sbin/pidof java) ]; do :; done

        rm -f /var/lock/subsys/tomcat

    else

        echo "tomcat not running"

    fi

}



case "$1" in

    start)

        start

        ;;

    stop)

        stop

        ;;

    restart)

        stop

        start

        ;;

    status)

        /usr/local/tomcat/bin/catalina.sh version

        ;;

    *)

        echo "Usage: $0 {start|stop|restart|status}"

esac



exit 0

これで完了です。起動したか確認。

サーバ起動。

sudo service tomcat start

起動したら8080ポートでアクセスしてください。tomcat 画面が出るはずです。

http://192.168.33.10:8080

vagrant で仮想環境構築し javatomcat をインストールし java アプリケーションを動かす環境を整えました。ローカル環境の Vagrantfile のあるディレクトリと tomcat の webappとを同期すれば簡単にデプロイできます。ちなみに jenkins を入れればローカルで CI 環境も実現できますね。

 

FMDB でカラムの存在確認

iOS 開発でローカル DB を操作する際に利用できる FMDB について紹介していきます。

自分が開発しているアプリでは FMDB ではなく社内のライブラリを使って DB 操作を行っています。ただそのアプリのローカル DB のテーブルにカラムを追加する必要があり、すでにアプリをインストール済の端末に対しては DDL 文を発行しなければなりませんでした。そのため、対象のカラムがローカル DB の該当テーブルに存在するか判定する処理が必要でした。そこで、FMDB を使ってそのあたりの処理を行いました。

準備すること
  1. libaqlite3.0.dylib を Xcode プロジェクトに追加
  2. github からダウンロード
  3. 展開し /src/fmdb 配下にソースファイルがあることを確認
  4. 上記ファイルを Xcode プロジェクト内に配置する

   f:id:tunanosuke:20140508141417p:plain

これで準備完了です。あとは必要に応じて使いたいときにヘッダーファイルを import して利用できます。

カラム存在確認

1.ファイルインポート

#import "FBDatabase.h"

2. DB 接続

NSArray *paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
NSString *dir = [paths objectAtIndex:0];
FMDatabase *db = [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"user.db"]];

3. DB の開閉

// DBオープン
[db open];
 ・
// 何らかの処理
 ・
// DBクローズ
[db close];

4.カラムの存在確認

BOOL isHoge = [db columnExists:@"columnName" inTableWithName:@"tableName"];
まとめ

 社内のライブラリではなくローカル DB 操作は FMDB で十分。。。

 

オブジェクト生成時の*とは

Objective-C や C でオブジェクト生成時に"*"を付けます。普段当たり前のように利用しているため何の疑問もなく付けていますが、このケースに限らず自分の中で当たり前に習慣化していることについて質問や説明を求められたとき、「あれっ、そういえば」と思うことってあったりしたことがあるかと思います。今回は Objective-C では当たり前のように付ける"*"についてまとめてみました。

Objective-C では変数宣言するとき以下のように行います。

NSString *tuna = @"tunanosuke blog"

NSString *型の tuna という変数を宣言しています。これはポインタとよばれ、"*"を付けることで tuna という変数は文字列そのものを格納しているのではなく、文字列が格納されているポインタ(住所)を格納しています。Objective-C オブジェクトのためのメモリはヒープ空間から確保されます。スタック上に作られることはないです。そのため、"*"を付けて宣言する必要があります。以下の宣言はもちろんですが、エラーになります。

NSString tuna = @"tunanosuke blog"

図にするとこんな感じ。

f:id:tunanosuke:20140501172903p:plain

スタック領域はプリミティブ型データ、ポインタを確保。ヒープ領域はクラスの実態を保持します。またスタック領域はそのまま領域が使えるのでアクセスが高速です。ヒープ領域は図のようにポインタから実態を見るので若干ですが速度が落ちます。

スタック領域は自動的にクリーンアップされます。それに対しヒープ領域に確保されたメモリは直接管理する必要があります。ただ、Objective-C では ARC 機能がありますので、ある程度抽象化はされています。

基本的なことではありますが、"*"についての内部的な部分についてまとめてみました。