先日、iOS5の日本語キーボード対応を行いましたが、1点問題がありました。
開発環境はiOS5.0 + Xcode4.2を使用しており、ビルドターゲットSDKiOS4.0で作成していました。
開発機はiOS4.2とiOS5.0の端末を使用していたので問題なかったのですが、iOS4.1の端末で動作確認をした所、起動時にBAD ACCESSが発生していました。
原因を調査したところ、キーボードの言語が変わったことを表す、UITextInputCurrentInputModeDidChangeNotificationがダメだったようで。


定義を確認したところ、

UIKIT_EXTERN NSString *const UITextInputCurrentInputModeDidChangeNotification __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_2);

このようにiOS4.2以降でないとダメだったようです。
なので端末のiOSバージョンを取得し、iOS4.2以上であれば処理するようにしました。
盲点でした・・・。

/*!
 * @brief	View表示通知
 */
- (void)viewWillAppear:(BOOL)animated {
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];

	const float version = [[[UIDevice currentDevice] systemVersion] floatValue];
	if (version >= 4.2f) {
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeNotification:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
	}
}

/*!
 * @brief	View非表示通知
 */
- (void)viewWillDisappear:(BOOL)animated {
	[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];

	const float version = [[[UIDevice currentDevice] systemVersion] floatValue];
	if (version >= 4.2f) {
		[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextInputCurrentInputModeDidChangeNotification object:nil];
	}
}

iOS4.1はiOS5.0 + Xcode4.2ではデバッグはサポートされていない?らしく最初にオーガナイザの方でインストールするかの確認ダイアログが表示されました。

先日iOS5がリリースされましたが、キーボードの処理で変更があったため、対応したメモ。
iOS4(iPhone/iPod touch)まではキーボードの高さは216だった。
しかし、日本語キーボードの予測変換がキーボードの上に表示されるようになり、その際には高さが252と変更された。
キーボードの上にツールバーを設置していたため日本語キーボードに切り替えた場合、予測変換でツールバーが見えなくなってしまうという現象に陥ったため対応したのが経緯。
以下、キーボードの上に高さ40のツールバーを設置した時の処理




※2011/10/26 更新
iOS5でのキーボード処理について【追記】 - NoasMarkのプログラムMemo(まれに雑記)

/*!
 * @brief	共通処理
 */
- (void)KeyboardCommonFunc:(NSNotification *)notification {
	CGRect keyboardRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
	NSLog(@"keyboardRect.size.height = %f", keyboardRect.size.height);

	if (keyboardRect.size.height > 0.f) {
		// ツールバーの高さが40の場合. (画面サイズは固定だがサンプルなので320x480として扱う).
		[toolBar setFrame:CGRectMake(0, 480 - keyboardRect.size.height - 40, 320, 40)];
	}
}

/*!
 * @brief	キーボード表示通知
 */
- (void)keyboardWillShowNotification:(NSNotification *)notification {
	NSLog(@"keyboardWillShowNotification call");
	[self KeyboardCommonFunc:notification];
}

/*!
 * @brief	キーボード変更通知
 */
- (void)keyboardWillChangeNotification:(NSNotification *)notification {
	NSLog(@"keyboardWillChangeNotification call.");
	[self KeyboardCommonFunc:notification];
}

/*!
 * @brief	View表示通知
 */
- (void)viewWillAppear:(BOOL)animated {
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];

	const float version = [[[UIDevice currentDevice] systemVersion] floatValue];
	if (version >= 4.2f) {
		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeNotification:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
	}
}

/*!
 * @brief	View非表示通知
 */
- (void)viewWillDisappear:(BOOL)animated {
	[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];

	const float version = [[[UIDevice currentDevice] systemVersion] floatValue];
	if (version >= 4.2f) {
		[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextInputCurrentInputModeDidChangeNotification object:nil];
	}
}

言語が変わった時はキーボードの高さ情報は0のようなので、サイズ判定をして0以上ならツールバーの位置を変更するようにしている。
言語が変わった場合にツールバーの情報を変えたりする場合はUITextInputCurrentInputModeDidChangeNotificationで指定したメソッドで処理をすれば問題ないかと。

以下、変更した時のログ。

// 1.
// 表示(日本語キーボード).
keyboardWillShowNotification call
keyboardRect.size.height = 252.000000
keyboardWillShowNotification call
keyboardRect.size.height = 252.000000

// 2.
// 日本語→英語.
keyboardWillChangeNotification call
keyboardRect.size.height = 0.000000
keyboardWillShowNotification call
keyboardRect.size.height = 216.000000

// 3.
// 英語→日本語.
keyboardWillChangeNotification call
keyboardRect.size.height = 0.000000
keyboardWillShowNotification call
keyboardRect.size.height = 252.000000

プログラムで配置するのもいいけど、XMLで配置したほうが編集もしやすかったりするので用途に応じて私は利用しています。
今回は会員登録用の情報入力ダイアログを作成するとして、EditText、RadioGroup、DatePicker、Buttonを利用したサンプルを。

表示されるのは下のような感じ。



regist_dialog.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	android:orientation="vertical">

	<!-- タイトル -->
	<TextView android:text="プロフィール登録"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textColor="#FFFFFF"
		android:textSize="20sp"
		android:gravity="center"
		android:layout_marginBottom="10dp">
	</TextView>

	<!-- ニックネーム -->
	<TextView android:text="ニックネーム(10文字まで)"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textColor="#FFFFFF"
		android:textSize="14sp"
		android:layout_marginLeft="20dp">
	</TextView>

	<EditText android:id="@+id/edittext_nickname"
		android:inputType="text"
		android:maxLength="10"
		android:layout_marginLeft="20dp"
		android:layout_marginRight="20dp"
		android:imeOptions="actionDone"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content">
	</EditText>

	<View android:layout_width="wrap_content" android:background="#DDFFFFFF" android:layout_height="1dp" android:layout_marginBottom="1dp"></View>

	<!-- 性別 -->
	<TextView android:text="性別"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textColor="#FFFFFF"
		android:textSize="14sp"
		android:layout_marginLeft="20dp">
	</TextView>

	<RadioGroup android:id="@+id/radiogroup_sex"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:orientation="horizontal"
		android:layout_marginLeft="20dp">

		<RadioButton android:id="@+id/radio_male"
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:text="男性">
		</RadioButton>

		<RadioButton android:id="@+id/radio_female"
			android:layout_width="wrap_content"
			android:layout_height="wrap_content"
			android:layout_marginLeft="20dp"
			android:text="女性">
		</RadioButton>
	</RadioGroup>

	<View android:layout_width="wrap_content" android:background="#DDFFFFFF" android:layout_height="1dp" android:layout_marginBottom="1dp"></View>

	<!-- 年齢 -->
	<TextView android:text="生年月日"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:textColor="#FFFFFF"
		android:textSize="14sp"
		android:layout_marginLeft="20dp">
	</TextView>

	<DatePicker android:id="@+id/datepicker_birthday"
		android:layout_height="wrap_content"
		android:layout_width="wrap_content"
		android:layout_gravity="center">
	</DatePicker>

	<!-- 登録 -->
	<Button android:id="@+id/button_regist"
		android:layout_width="fill_parent"
		android:layout_height="wrap_content"
		android:layout_marginTop="10dp"
		android:layout_marginBottom="10dp"
		android:layout_marginLeft="20dp"
		android:layout_marginRight="20dp"
		android:text="登録">
	</Button>
</LinearLayout>

AccountRegistDialogクラス

package com.yourcompany.sample;

import android.app.Activity;
import android.app.Dialog;
import android.os.Bundle;
import android.view.Window;
import android.view.KeyEvent;
import android.view.ViewGroup;
import android.view.View;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioGroup;
import android.widget.DatePicker;
import android.widget.DatePicker.OnDateChangedListener;
import android.content.Context;
import java.util.Calendar;

/*!
 * @brief	AccountRegistDialog
 */
public class AccountRegistDialog extends Dialog {
	private static final LinearLayout.LayoutParams FILL = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT);

	private Context		m_context;
	private DialogListener m_listener;
	private Button		m_btnRegist;
	private EditText	m_editNickName;
	private RadioGroup	m_radioSex;
	private DatePicker	m_datePicker;

	/*!
	 * @brief	コンストラクタ
	 *
	 * @param	activity	Activity
	 * @param	listener	リスナー
	 */
	public AccountRegistDialog(Activity activity, DialogListener listener) {
		super((Context)activity);

		m_context = (Context)activity;
		m_listener = listener;
		m_btnRegist = null;
		m_editNickName = null;
		m_radioSex = null;
		m_datePicker = null;
	}

	/*!
	 * @brief	生成イベント
	 */
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);

		LayoutInflater factory = LayoutInflater.from(m_context);
		final View entryView = factory.inflate(R.layout.regist_dialog, null);

		// 登録ボタン.
		m_btnRegist = (Button)entryView.findViewById(R.id.button_regist);
		m_btnRegist.setOnClickListener(new View.OnClickListener() {
			public void onClick(View v) {
				m_listener.onRegistSelected();
				dismiss();
			}
		});

		// ニックネーム入力.
		m_editNickName = (EditText)entryView.findViewById(R.id.edittext_nickname);
		m_editNickName.setText("");

		// ラジオボタングループ.
		m_radioSex = (RadioGroup)entryView.findViewById(R.id.radiogroup_sex);
		m_radioSex.check(R.id.radio_male);
		m_radioSex.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
			public void onCheckedChanged(RadioGroup group, int checkedId) {
				if (checkedId == R.id.radio_male) {
					// 男にチェック.
				} else if (checkedId == R.id.radio_female) {
					// 女にチェック.
				}
			}
		});

		// 日付入力.
		m_datePicker = (DatePicker)entryView.findViewById(R.id.datepicker_birthday);
		m_datePicker.init(Calendar.getInstance().get(Calendar.YEAR),
							Calendar.getInstance().get(Calendar.MONTH),
							Calendar.getInstance().get(Calendar.DAY_OF_MONTH),
							new DatePicker.OnDateChangedListener() {
								public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
								}
							});

        addContentView(entryView, FILL);
    }
    
	/*!
	 * @brief	キー処理
	*/
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_BACK) {
			m_listener.onCancel();
			dismiss();

			return true;
		}

		return super.onKeyDown(keyCode, event);
	}

	/*!
	 * @brief	ダイアログ処理の結果を通知するためのインターフェース
	 */
	public static interface DialogListener {
		/*!
		 * @brief	登録ボタン押下
		 */
		public abstract void onRegistSelected();

		/*!
		 * @brief	キャンセル
		 */
		public abstract void onCancel();
    }
}



使用例

public class SampleActivity eve extends Activity {
	// 省略.

	private void showRegistDialog() {
		AccountRegistDialog dialog = new AccountRegistDialog(this, new ResultListener());
		dialog.show();
	}

	public class ResultListener implements AccountRegistDialog.DialogListener {
		public void onRegistSelected() {
			// 登録を選択した際の処理.
		}

		public void onCancel() {
			// キャンセルした際の処理.
		}
	}
}

便利ですけど、iOSのInterfaceBuilderに比べたら・・・

ウォールへの投稿には下記のようにダイアログを使用しての投稿もできます。

m_Facebook.dialog(this, "stream.publish", new WallPostDialogListener());

しかし、アプリ側から何かをウォールへ投稿する場合には使いづらいです。

AsyncFacebookRunner#request(final String graphPath, final Bundle parameters, final String httpMethod, final RequestListener listener, final Object state)

ウォールの取得でも使った上記のメソッドで可能です。

// 投稿.
Bundle bundle = new Bundle();
bundle.putString("message", "投稿テスト");
m_AsyncRunner.request("me/feed", bundle, "POST", new WallPostRequestListener(), null);

//---------------------------------------------------------------------

// 返信.
Bundle bundle = new Bundle();
bundle.putString("message", "返信");

// 返信をするオブジェクトIDを指定.
m_AsyncRunner.request("XXXXXXXXXXXXXX_XXXXXXXXXXXX/comments", bundle, "POST", new WallPostRequestListener(), null);

//---------------------------------------------------------------------

/*!
 * @brief	WallPostRequestListener
 */
public class WallPostRequestListener implements AsyncFacebookRunner.RequestListener {
	public void onComplete(final String response, Object state) {
	}

	public void onFacebookError(FacebookError e, Object state) {
	}

	public void onFileNotFoundException(FileNotFoundException e, Object state) {
	}

	public void onIOException(IOException e, Object state) {
	}

	public void onMalformedURLException(MalformedURLException e, Object state) {
	}
}

Facebook SDK for Androidの仕様だけでなく、Facebook APIの仕様も把握していないと気づきませんでした。

Facebook SDK for Androidを使用してのサンプルを今日から少しずつ書いていこうかと。
SDKの導入に関しては下記のリンクを参照。


Facebook API 入門


Facebookでユーザーのウォールやニュースフィードを取得するにはログイン処理が必要だが今回は割愛。
次回以降に記述する予定です。


以下、サンプル



/*!
 * @file
 *
 * @brief	facebook ウォール/ニュースフィード取得サンプル
 *
 * @author	NoasMark
 *
 * @date	2011/08/24
 */
package jp.noasmark.sampleActivity;

import java.net.URLDecoder;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.view.Window;
import android.os.Bundle;
import org.json.*;
import com.facebook.android.*;
import com.facebook.android.AsyncFacebookRunner.RequestListener;

/*!
 * @brief	sampleActivity
 */
public class sampleActivity extends Activity {
	private static final String[] PERMISSIONS = new String[] {
			"publish_stream",
			"read_stream",
			"offline_access",
	};

	private ProgressDialog		m_prgDialog;
	private Facebook			m_Facebook;
	private AsyncFacebookRunner	m_AsyncRunner;

	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		m_Facebook = new Facebook("FACEBOOK_APPLI_ID");
		m_AsyncRunner = new AsyncFacebookRunner(m_Facebook);

		// ----------------------------.
		// ログイン処理は省略.
		// ----------------------------.

		showLoadingDialog();
		m_AsyncRunner.request("me/home", new FacebookRequestListener());	// ニュースフィード情報取得.
		//m_AsyncRunner.request("me/feed", new FacebookRequestListener());	// ウォール情報取得.
	}

	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		m_Facebook.authorizeCallback(requestCode, resultCode, data);
	}

	public void showLoadingDialog() {
		m_prgDialog = new ProgressDialog(sampleActivity.this);
		m_prgDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
		m_prgDialog.setMessage("Loading...");
		m_prgDialog.show();
	}

	public class FacebookRequestListener implements AsyncFacebookRunner.RequestListener {
		public void onComplete(String response, Object state) {
			m_prgDialog.dismiss();

			try {
				final JSONObject json = new JSONObject(response);
				
				if (json != null) {
					JSONArray data = json.getJSONArray("data");
					final int nLoopLimit = (data != null ? data.length() : 0);

					for (int i = 0; i < nLoopLimit; ++i) {
						JSONObject obj = data.getJSONObject(i);

						if (obj != null) {
							// OBJECT_ID.
							String strObjectID = obj.getString("id");

							// ユーザーIDとユーザー名.
							JSONObject from = obj.getJSONObject("from");
							String strID = from.getString("id");
							String strName = from.getString("name");

							String strMsg = "";

							// メッセージが無い場合もある.
							try {
								strMsg = URLDecoder.decode(obj.getString("message"));
							} catch (Exception e2) {
							}
						}
					}
				}
			} catch (Exception e) {
			}
		}

		public void onFacebookError(FacebookError e, Object state) {
			m_prgDialog.dismiss();
		}

		public void onFileNotFoundException(FileNotFoundException e, Object state) {
			m_prgDialog.dismiss();
		}

		public void onIOException(IOException e, Object state) {
			m_prgDialog.dismiss();
		}

		public void onMalformedURLException(MalformedURLException e, Object state) {
			m_prgDialog.dismiss();
		}
	}
}

Facebook SDK for AndroidのPOST処理でエラーが発生し始めたので調べたら
com.facebook.android.Util内でArrayIndexOutOfBoundsExceptionが出ていたので調査。

decodeUrl()でv[1]を参照しようとしてエラーになってたっぽい。
ログを見るとpost_id=xxxxxxxxで1ループで終わると思ったのだが末尾になんかデータが付き始めてた。
String v[]の要素数を判定して、2つだったらputするようにして回避

    public static Bundle decodeUrl(String s) {
        Bundle params = new Bundle();
        if (s != null) {
            String array[] = s.split("&");
            for (String parameter : array) {
                String v[] = parameter.split("=");
                if (v.length == 2) {
                    params.putString(URLDecoder.decode(v[0]), URLDecoder.decode(v[1]));
                }
            }
        }
        return params;
    }

でもなんで急に?

気がついたら1年以上更新していなかった。
iPhone開発も終え、書くネタがなかった。
そしてめんど(ry
最近MacBook proを買い、BootCampでWindows7を導入したので
そのメモでも。

                                                                                                                        • -

  1. Windows 7のインストールディスクをセットしておく
  2. アプリケーションからBootCampを起動
  3. Windows用のパーティションMac用のパーティションを振り分ける
    • 自分はMac : Win = 4 : 6位で分けました。
    • Windowsでの作業が若干多いかなと思ったので。
  4. Windowsインストール開始
  5. 再起動されてWindowsのインストールが開始される
  6. インストール先のパーティション選択になる
    • ここでさっき振り分けた領域を選択しようとしてもなぜか選べない。
    • HDDのフォーマットがFAT32だから?
  7. 項目でフォーマットがあったのでインストール先をフォーマット
  8. あとはWindowsのインストールが完了するのを待つ
  9. インストールが完了したらWindowsが立ち上がる
  10. Windowsのインストールディスクを取り出す。
    • キーボードのイジェクトボタンを押しても反応は無い
    • まだドライブ等がインストールされていないので。
    • コンピュータのDVDドライブを選択をして「取り出し」でディスクが排出される
  11. Macのインストールディスクを入れる
  12. BootCampのインストールや各種ドライバのインストールが始まる。
    • ここで絶対にキャンセルをしてはいけないらしい。
  13. 完了
                                                                                                                        • -


結構すんなりといけた。

PCの電源投入後、どのOSを起動するかを選択するには
電源投入後にOptionキーを押しっぱなしにすればOS選択画面が出る。

Windowsの方でもタップするだけでクリック判定にしたり、副ボタンの設定も出来る。
重要なのがファンクションキーの設定がデフォルトでOFFになっているので
いちいちFnキーを押しながらやらないといけない。
Windowsに使い慣れている自分では面倒なので即ファンクションキーの設定を有効にした。

所要時間はだいたい3時間位。