Pages

2013年10月20日日曜日

iOSアプリにAdMobを表示する

数年ぶりにiPhoneアプリにAdMobを導入しようとしたらハマりました。

私がAdMobを導入するに当たってハマったポイントは3つなんですが、
私の開発環境が悪いのか、そのポイント全てを網羅したページがなかったのでここにまとめておきます。

ちなみに、AdMobの公式ドキュメントを見ながら組み込んでハマりました。

私の読解力が自分で見積もっているよりも低くくない限り、あのドキュメントだけで広告表示ができるとは思いません

というわけで、AdMobの導入方法(最も単純な方法で)。

まず、このページからAdMobのSDKをダウンロードします。
もちろんiPhoneアプリに導入するワケですから、iOS版を選んでください。

ダウンロードしたら、とりあえず公式の導入ページを見てみましょう。
このページの「SDKの導入」を進めて行っても広告の表示はできません。
というか、コンパイルすらできません

ってな訳で、「SDKの導入」を読み進めつつ、どの箇所を注意すべきかを書いていきます。

自分のプロジェクトにSDKを導入します。
先ほどダウンロードしたSDKを解凍すると色々入ってます。
この内、「Add-onsフォルダ」と「README.txt」を除く全てのファイルを自分のプロジェクトにインポートします。

ちなみに、公式の手順書には以下のように記述されています。

SDK を解凍すると、6 つの Objective-C ヘッダー、ランタイム ライブラリ、README が生成されます。

はい。神経質な人ならこの文言でもハマるかもしれません。
なんせ Objective-Cヘッダー は9個ありますからね
しかし、まだハマるような箇所ではありません。

次に進みましょう。

フレームワークの追加
ここ要注意です。
手順書では

・AudioToolbox
・MessageUI
・SystemConfiguration
・CoreGraphics

4つのフレームワークを追加すればOK 

って書いてますけど、ウソです。
少なくとも私の環境ではウソでした。
この4つだけだと、Mach-O Linker Error が出てBuild Failedになりました。

上記のフレームワーク以外にも次の2つのフレームワークの追加が必要です。
・AdSupport.framework
StoreKit.framework

手順書のフレームワーク追加の画像をよくみると、上の2つのフレームワークも追加されているという…


GADBannerViewの追加
ここは特にハマるような事ではないのですが、ややこしい箇所があるので書いておきます。

ソースコードの中に、自分の広告ユニットIDを指定する箇所があります。
// 広告の「ユニット ID」を指定する。これは AdMob パブリッシャー ID です。
  bannerView_.adUnitID = MY_BANNER_UNIT_ID;

AdMobの自分のページには「パブリッシャーID」と「広告ユニットID」が存在します。
パブリッシャーIDは「pub-01234567xxxxxx」のような感じで、
広告ユニットIDは「ca-app-pub-01234567xxxxxx/0123456789」という感じです。
(あくまで私の形式なので、もしかしたらもっと違う形式があるかもしれません。)

//広告の「ユニットID」を指定する。これはAdMobパブリッシャーIDです。

どっちやねん。
ここで指定するのは広告ユニットIDです。
パブリッシャーIDではエラーとなり、広告が表示されません。

後は、導入手順書通りにプログラムしていきます。
が、コンパイルしてアプリが起動して、広告が表示されるタイミングでSIGABRTでアプリが落ちます。
落ちなければそれでいいのですが、私の環境では落ちました。

ここで同じ現象になる場合は、アプリの「Build Settings」の「Other Linker Flags」に「-ObjC」を追加してください。

これでやっと広告が表示されるようになります。

いやー、めんどくさい。
お疲れさまでした。
頑張って広告収入で稼ぎましょう!!





2013年10月1日火曜日

cronとORA-01861

groovyを実行してDBからデータを引っ張ってくるというシェルスクリプトを書いたんですが、このスクリプトをcronで実行すると

ORA-01861: literal does not match format string

というエラーが発生。
もちろん、cronを通さず実行すると特に問題なく動作する。

実際にエラーが発生している箇所を見ると

SELECT xxxx (省略) WHERE ADMIT_DATE < TO_DATE('20131001', 'YYYYMMDD')

こんな感じのSQLを実行している箇所で、Oracleのエラーは

「文字列と日付を比較すんな」

って感じのエラー。
これが、何でcronからの実行時にだけ発生するのか原因が分からずハマリました。


で、まぁ原因なんですが文字コードなんですよね。

スクリプトで環境変数のLANGをログに出すようにして実行(cronを通さずに実行)すると、

LANG="ja_JP.UTF-8"

なんですよ。
で、cronでスクリプトを実行するとLANGには何も設定されてなかったんですね。

通常ユーザでは、
何も設定していない場合は
/etc/sysconfig/i18n
に設定されているOSの文字コードが引き継がれるらしいんですけど(.bashrcとかに書いてたらそっち見ますけど)、どうやらcronではユーザの環境変数を引き継がないみたい。

何か設定があるかもしれませんが。。。

なので、cronでも文字コードを同じようにするために以下の行をスクリプトに追加

export LANG=ja_JP.UTF-8

無事にcronから動きましたとさ。


2013年9月30日月曜日

thunderbolt ファームウェア1.2

かなり前からApp Storeで「thunderbolt ファームウェア1.2」とかいうアップデートが表示されてて気になってたんですけど、アップデート→再起動すると再起動途中でステータスバーが表示されてすぐ電源が切れるという現象になっていたので諦めていました。

けど、今日やっとアップデートの仕方が分かったんですよっ!

アップデートが表示されてから数ヶ月。これでやっとApp Storeのが消える!!

で、そのやり方なんですけど

ACアダプターをつなぎながらアップデートする

たったこれだけ。。。。。。。。っざけんなよ。



2013年9月4日水曜日

cocos2d + UITapGestureRecognizer でどハマりした

cocos2dでUITapGestureRecognizerを使おうと以下の実装をAppDelegateに追加しました

GameLayer *layer = [[[GameLayer scene] children] objectAtIndex:0];
UITapGestureRecognizer *gestureRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:layer action:@selector(handleTapFrom:)] autorelease];
[viewController.view addGestureRecognizer:gestureRecognizer];

で、ゲームの画面遷移的には以下のような流れとしていました。

タイトル画面(こいつがAppDelegaterunWithSceneされる)
→ ゲーム画面(ここでタップを検出する。上でいうGameLayer
→ 終了画面 → タイトル画面

タイトル画面ではCCMenuItemImageを使ってメニューを作成して、そのメニューが押されたらゲームが始まるという流れです。

で、ここで終了画面からタイトル画面に戻ると、メニューを押しても反応しない
メニューは確実に押されている(というのもCCMenuItemImageは通常の画像と押されたときの画像を設定していて、メニューを押したときにちゃんと切り替わっている)のにも関わらず、selectorで設定したメソッドが呼ばれない。という状態に陥りました。

エラーが出るなら何とかなるんですが、ただただ反応しないだけなので、何が原因か全く分からない。

まぁ、UITapGestureRecognizerだろうとあたりを付けて、その箇所を削除してみたらちゃんと動きますよ。

UITapGestureRecognizerがCCMenuItemをよりも先に反応してるんだろうなーというのは何となく想像ついたんですけど、どうしたらそれを回避できるのかの対策にどハマリしました。

開発時間は1日2時間程度とれたら良い方なので、正直4日くらい彷徨いました。

んで、何かの拍子にstackoverflowにかなり良く似た現象を発見(それまでもstackoverflow見てたのに発見できなかったという)。

で、対策として如何ほど難しい実装をするんだろうと、その質問の回答をみたところ

「OK、言いたいことは分かった!
gestureRecognizer.cancelsTouchesInView = NO;
と書いてみなっ!!」(訳:オレ)

って3行が…

で、実際に意図する状態に出来ましたとさって話でした。

分かった後でcancelsTouchesInViewについて色々調べると、これ結構有名な話でして、いかに自分の検索脳力の低いかを痛感した次第で。。。

まぁ、実際にはコレだけ書いたらOKって訳ではないのですが、
そのあたりはcancelsTouchesInViewでググると嫌味なほどヒットするので、その中からいい感じのやつを探してくださいな。

日々修行ですね。やっぱ。



2013年5月29日水曜日

GALAXY S4 (SC-04E) のUser-Agent

docomoの2トップの一角である「GALAXY S4」くんですが、やってくれました。

GALAXYの家系はちょいちょい開発者を悩ます独自仕様を出してくるんですが、今回はUser-Agentを変えてきました。


これまでのAndroidのUAは、例えばGALAXYの家系のSC-03Eは
Mozilla/5.0 (Linux; U; Android 4.1.1; ja-jp; SC-03E Build/JRO03C) ......
という感じでしたが、SC-04Eは以下のようになりました
Mozilla/5.0 (Linux; Android 4.2.2; ja-jp; SC-04E Build/JDQ39) ......

Linux; U; Androi.....

「U;」が消えました。

まぁ、今までも発表されている以外のUAを思いっきり送りつけて来てましたけど、デフォルト的なUAに変更はありませんでした。

が、

今回は、docomoが発表しているページを見てもUAから完全にU;が消えましたね。

なんて言うんですかねー、もうそれぞれの端末が独自仕様を色々入れてくるのやめてほしい。
正直スマホのガラケー化だと思うんですよね。。。。。

2013年4月27日土曜日

proxy環境下でUnityを使用する

Unityなんですが、インストール後の初の起動時に何やら認証をするために外部にアクセスしているみたいです。

proxy環境下にある場合、proxyが邪魔をしてUnityが認証できず起動に失敗してしまいます。
まぁ、そんな環境下でUnityを使う人がどれだけ存在するか知りませんが。。。

WindowsのUnityの場合、環境変数HTTP_PROXY、HTTPS_PROXYを見ているようで、
起動前にこれらの環境変数を設定する必要があります。

Unityが初期起動に失敗する場合お試し下さいませ。

2013年4月11日木曜日

iPhoneのクラッシュログの見方の補足

現在リリースしている無料アプリがあるんですが、広告システムを更新するためにライブラリの変更を行いました。
アップルの審査を無事通過し「Ready for Sale」となったので、特にDLして確認せずそのままにしておきました。

数日後、DL数の激減と評価が星一つばかりになっているのに気付き慌ててDLして確認してみた所、なんと起動しないという状態に。。。

実機ではiPhone4S、iPhone5共に動作確認していましたが、まさか事態にすぐにクラッシュログを確認することにしました。

iPhoneのクラッシュログなんですが、そのままでは何処が問題で落ちたのか分からないので、Xcodeに用意されているスクリプトを使ってゴニョゴニョやるのは知っていたんですが、いろんなブログに書いてあるやり方をしても何故か
Error: "DEVELOPER_DIR" is not defined at 
とか言われる。

いや、DEVELOPER_DIRなんか定義してないよ。ってか、どこよそれ。
って思ってたらスクリプトファイルが置いてあるちょっと上のディレクトリがDeveloperってディレクトリだったのでとりあえず↓みたいにしてみた

export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer/"

もう一度実行。
$ ./symbolicatecrash xxxx.crash xxxx.app.dSYM > hoge.dat
無事出力されました。

余談ですが、クラッシュログを見ても原因が何とも言えなかったので、Appleに質問することにしました。
今まで何度か質問したことがあるのですが、私が頑張って英語メールを送ってもいつも返信が日本語だったので、今回日本語でメールを投げてみたところ、
「質問は英語でしか受付ないぜ」
的な英文メールが届きましたとさ。

2013年4月9日火曜日

ViewPagerで画面数に合わせた背景画像の移動を実現する(後編)

前回の続きで、ViewPagerでページを切り替える度に背景が移動していく箇所を実装します。

ここでの大雑把な仕様は、
・最初のページは全ページの真ん中のページ(例えば全5ページなら3ページが初期位置)
・背景画像は必ず横長の画像
・背景は横にしか移動しない
ということにします。

ちなみに私はAndroidに関しては素人なので、この実装よりもいいやり方があれば是非是非教えて頂きたい。
というか、こんな実装を必要とする人っているのか??

それでは、とりあえず実装後のMainActivityはこんな感じとなります。
public class MainActivity extends Activity {
 private CustomAdapter adapter;
 
 private ViewPager mViewPager;
 private ImageView mImageView;
 private Bitmap mBitmap;
 
 private float displayW;
 private float displayH;
 
 // -----追記
 private Matrix matrix;
 private float currentBmpX;
    private int maxMoveBmpX;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        
        // カスタム PagerAdapter を生成
        adapter = new CustomAdapter(this);
        adapter.add(Color.RED);
        adapter.add(Color.GREEN);
        adapter.add(Color.BLUE);
        adapter.add(Color.BLACK);
        adapter.add(Color.WHITE);
 
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        displayW = metrics.widthPixels;
        displayH = metrics.heightPixels;
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 画像そのものは読み込まない
        BitmapFactory.decodeResource(getResources(), R.drawable.wall, options);
        
        // dpi計算
        int bmpNativeHeight = options.outHeight;
        float dpiRatio = bmpNativeHeight / displayH;
        float calcDpi = metrics.densityDpi * dpiRatio;
        if (mBitmap == null || mBitmap.isRecycled()) {
            options.inJustDecodeBounds = false;
            options.inDensity = (int) calcDpi;
            mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.wall, options);
        }
        
        
        mImageView = new ImageView(this);
        mImageView.setImageBitmap(mBitmap);
        mImageView.setScaleType(ScaleType.MATRIX);
        addContentView(mImageView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        
        // ViewPager を生成
        mViewPager = new ViewPager(this);
        mViewPager.setAdapter(adapter);
        
        // -----追記
        matrix = new Matrix();
        currentBmpX = (mBitmap.getScaledWidth(metrics) / 2) - (displayW / 2);
        maxMoveBmpX = (int) (mBitmap.getScaledWidth(metrics) - displayW);
        mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
         private int currentPage;
         
   @Override
   public void onPageSelected(int pageNumber) {
    currentPage = pageNumber;
    currentBmpX = getAbsPositionX(pageNumber);
   }
   
   @Override
   public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    if (positionOffsetPixels == 0) {
     return; 
    }
    
    //page移動中の処理
    float scaledOffsetPixels = positionOffsetPixels * getScaleValue();
    
    // ページ毎の始点のx座標
    float absolutePosition = getAbsPositionX(position);
    currentBmpX = absolutePosition + scaledOffsetPixels;
    
          mImageView.setImageMatrix(getMatrix(matrix, -currentBmpX));
          mImageView.invalidate();
   }
   
   @Override
   public void onPageScrollStateChanged(int state) {
   }
   
   protected float getAbsPositionX(int page) {
    return (maxMoveBmpX / (adapter.getCount() - 1)) * page;
   }
  });
        
        addContentView(mViewPager, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }
    
    @Override
    protected void onResume() {
     super.onResume();
     mViewPager.setCurrentItem((adapter.getCount()/2), true);
     mImageView.setImageMatrix(getMatrix(matrix, -currentBmpX));
    }
    
 protected Matrix getMatrix(Matrix matrix, float xPosition) {
     if (xPosition > 0) {
      xPosition = 0;
     }
     if (Math.abs(xPosition) > maxMoveBmpX) {
      xPosition = -maxMoveBmpX;
     }
      
     matrix.setTranslate(xPosition, 0);
     return matrix;
    }
    
 public float getScaleValue() {
  float displayWidth = getWindowManager().getDefaultDisplay().getWidth();
  float fullScreenSize = displayWidth * (adapter.getCount()-1); 
  return maxMoveBmpX / fullScreenSize;
 }
}

前回と比較して、onCreate()で今回追加実装した箇所には「// -----追記」とコメントしています。
上の行番号で言うと、
11〜14行目
59〜96行目
その他のメソッドは今回追加実装してます。

まぁ、これだけで読む気も失せてくるんですが、説明を(今後の自分の為にも)。
まず、61行目と62行目の
currentBmpX = (mBitmap.getScaledWidth(metrics) / 2) - (displayW / 2);
maxMoveBmpX = (int) (mBitmap.getScaledWidth(metrics) - displayW);
ですが、
currentBmpXは、画像の左上を原点としたときの、現在画面に表示されている背景画像の左上のX座標です。
これの初期位置(つまり、中央のページで表示される背景画像の左上のX座標)を計算しています。
分かりにくいけど、計算で出しているのは上の画像の赤い点の位置ですね。
次に、maxMoveBmpXですが、これは最後のページで表示される背景画像のX座標の位置となります。

まぁ、このへんは特にポイントでも何でもないです。
今回のポイントとなる実装はViewPagerのsetOnPageChangeListenerですね。
上記コードの63行目〜96行目の部分です。

このListnerはページが動いているときにその状態を通知しまくってくれるので、その情報を使って背景画像の位置をImageView.setImageMatrixでズラしていくって感じですね。

OnPageChangeListenerでのポイントといえばポイントなんですが、
ページが進むと背景画像がマイナス方向(左方向)に移動する点。
何か、感覚的に理解しにくいのは私だけ??
それ以外は特にポイントってものは無いですね。実装も大したことしてないし。

あとポイントじゃないけど74行目〜76行目のif文。
別にこれなくても普通に動くんですけど、ページ移動中にそのときの移動距離が引数で渡ってくるんですが(positionOffsetPixels)、ページ移動が完了してからも何故かしばらく0が渡ってくるので、余計な処理を省く為に入れてます。
まぁ、なんとなくです。いらない気がしますね。。。。

長々と書きましたが、これでそれなりの動きになるはずです。
備忘録ついでに誰かのお役に立てれば幸いかと。

あ、変数名やメソッド名はあまり突っ込まないでね。

2013年4月8日月曜日

ViewPagerで画面数に合わせた背景画像の移動を実現する(前編)

Androidアプリで、ホーム画面のように左右のフリックで画面が切り替わり、その画面移動に追随して背景を移動させたい。

たったコレだけの事にアホみたいに時間を費やしてしまいました。

背景をページ数で割ってそれぞれのページの背景画像として設定したらいいじゃん。って簡単な話ではなかったんですよ、コレが。

今回、困ったのが次の仕様。
・画面数が動的に増減する。
・背景に使う画像のサイズは特に決まっていない。デザインによってサイズが違う。

この条件で、最初のページから最後のページまでの移動で、ぴったり画像の端から端まで移動させたい。

イメージはこんな感じ。

画面切り替えはViewPagerを使います。
んで、背景画像はアスペクト比をそのままに画面の高さに合わせることにします。

ViewPagerの詳細な使い方はググってください。
とりあえず、背景に画像を設定し、その上に簡単なViewPagerを重ねてみます。
ただし、背景を見えるようにしたいので、ViewPagerの背景は透明としています

public class CustomAdapter extends PagerAdapter {
    private Context mContext;
    private ArrayList mList;
 
    public CustomAdapter(Context context) {
        mContext = context;
        mList = new ArrayList();
    }
 
    public void add(Integer item) {
        mList.add(item);
    }
 
    @Override
    public Object instantiateItem(View container, int position) {
        // リストから取得
        Integer item = mList.get(position);
 
        // View を生成
        TextView textView = new TextView(mContext);
        textView.setText(position + "ページ");
        textView.setTextSize(50);
        textView.setTextColor(item);
        textView.setGravity(Gravity.CENTER);
        
        // コンテナに追加
        ((ViewGroup) container).addView(textView);

        // 背景を透明にする(第一引数が0ならOK)
        container.setBackgroundColor(Color.argb(0, 0, 0, 0));
        
        return textView;
    }
 
    @Override
    public void destroyItem(View container, int position, Object object) {
        ((ViewGroup) container).removeView((View) object);
    }
 
    @Override
    public int getCount() {
        // リストのアイテム数を返す
        return mList.size();
    }
 
    @Override
    public boolean isViewFromObject(View view, Object object) {
        // Object 内に View が存在するか判定する
        return view == (TextView) object;
    }
}


public class MainActivity extends Activity {

 private ImageView imageView;
 private Bitmap img;
 
 private float displayW;
 private float displayH;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // カスタム PagerAdapter を生成
        CustomAdapter adapter = new CustomAdapter(this);
        adapter.add(Color.RED);
        adapter.add(Color.GREEN);
        adapter.add(Color.BLUE);
        adapter.add(Color.BLACK);
        adapter.add(Color.WHITE);
 
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        displayW = metrics.widthPixels;
        displayH = metrics.heightPixels;
        
        // まずは画像を読み込まずに画像情報だけを取得する
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true; // 画像そのものは読み込まない設定
        BitmapFactory.decodeResource(getResources(), R.drawable.wall, options);
        
        // アスペクト比をそのままに、どの端末でも同じように表示させる(縦に合わせる)
        int bmpNativeHeight = options.outHeight;
        float dpiRatio = bmpNativeHeight / displayH;
        float calcDpi = metrics.densityDpi * dpiRatio;
        if (img == null || img.isRecycled()) {
            options.inJustDecodeBounds = false;
            options.inDensity = (int) calcDpi;
            img = BitmapFactory.decodeResource(getResources(), R.drawable.wall, options);
        }
        imageView = new ImageView(this);
        imageView.setImageBitmap(img);
        imageView.setScaleType(ScaleType.MATRIX);
        addContentView(imageView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        
        // ViewPager を生成
        ViewPager viewPager = new ViewPager(this);
        viewPager.setAdapter(adapter);
        addContentView(viewPager, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    }

ここでのポイントは、
・ViewPagerの背景を透明にする
・背景画像のImageViewのScaleTypeをMATRIXに指定する
となります。

背景は動きませんが、とりあえず動かしたい背景画像の上にViewPagerを乗せてみました。
ちなみにAndroidアプリをほどんど開発した事が無い私は、この「背景をどの端末でも同じに見えるようにdpiを合わせる」というのにも悩みまくりました。
これをしないと端末によって背景が拡大表示されたりするので注意してください。

次はViewPagerでページを切り替える度に背景が移動していく箇所を実装していきます。


2013年4月1日月曜日

以前から、ときどきiOS関係を書いてきましたが、仕事でもAndroidを触れる機会が増えたと同時にどうしてもまとめておきたい事も増えたので、とりあえず新しくブログを書こうと思います。

基本的にスマホ関係の技術を中心に書いていこうと思ってますが、
ちょくちょく料理のことも書いていこうかと(元料理人だけに)。

あとは何かオススメのガジェットとかあれば紹介しようかなーっと。
軽いノリで考えているのですが、少しでも同じような事で悩んでる人のお役に立てれば幸いかと。