UIApplicationDelegate のマルチタスキング関連メソッド調査

2010年8月31日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UIApplicationDelegate の調査記録。系統だった整理は末尾の参考情報サイトに良い記事があるのでそちらを参照のこと(状態遷移図など)。

※まとめはいつかやる(かもしれない)。
→ 良いまとめ記事をみつけた(のでやらない)。
下記の状態遷移を参照のこと:
Understanding iOS 4 Backgrounding and Delegate Messaging @ Dr. Touch
(2010-09-15追記)


調査内容


実機を使って各状況下での UIApplicationDelegate の呼び出しを調査した。

パターン
1. ホーム画面からの起動停止(基本パターン)
2. 起動後の動き(Fast App Switch切り替えなど)
3. 外部状況の変化(スリープ、電話,メモリ不足など)

条件
・アプリはマルチタスキング(UIApplicationExitsOnSuspend == NO) ※1
・実機 iPhone3GS / iOS 4.0.0
・MacBook Pro に接続し、オーガナイザの Consoleでログを確認(Xcode 3.2.3)

※1 [参考]Cocoaの日々: iOS 4.0 でアプリを一時停止しない設定 - UIApplicationExitsOnSuspend


コード例
UIApllicationDelegate の "Monitoring Application State Changes" で分類される各メソッドを実装した。
UIApplicationDelegate Protocol Reference

- (void)applicationWillTerminate:(UIApplication *)application {
  NSLog(@"applicationWillTerminate:(state=%d)", application.applicationState);
}

state=数値は、UIApplicationState の値を表している。
UIApplication Class Reference

抜粋(コメントはブログ著者が追記)
typedef enum {
   UIApplicationStateActive,      // 0
   UIApplicationStateInactive,    // 1
   UIApplicationStateBackground   // 2
} UIApplicationState;



以下、結果記録。


1. ホーム画面から起動・停止


初期状態:一度も起動したことが無い

起動(ホームボタン)

application:didFinishLaunchingWithOptions:
applicationDidBecomeActive:(state=0)

停止(ホームボタン)

applicationWillResignActive:(state=0)
applicationDidEnterBackground:(state=2)



2. 起動後の動き


初期状態:起動済みでホーム画面が表示(もしくは他アプリが起動)されている

[2A] Fast App Switch で起動

applicationWillEnterForeground:(state=2)
applicationDidBecomeActive:(state=0)

[2B] ホーム画面から起動

applicationWillEnterForeground:(state=2)
applicationDidBecomeActive:(state=0)

[2C] Fast App Switch で強制終了

applicationWillResignActive:(state=0)
applicationDidEnterBackground:(state=2)

com.apple.launchd[1] (UIKitApplication:com.yourcompany.
ApplicationDelegateSample[0x65c1][4003(UIKitApplication:
com.yourcompany.ApplicationDelegateSample[0x65c1]) Exited: Killed

SpringBoard[28] : Application 'ApplicationDelegateSample'
 quit with signal 9: Killed


[2D] 起動中に Fast App Switch を起動

applicationWillResignActive:(state=0)

a. その後、アプリへ復帰
applicationDidBecomeActive:(state=0)

b. その後、他アプリを起動
applicationDidEnterBackground:(state=2)


3. 外部状況の変化


初期状態:アプリをフォアグラウンドで起動中

[3A] スリープ

applicationWillResignActive:(state=0)
スリープから復帰
applicationDidBecomeActive:(state=0)

[3B] 電源OFF

applicationWillResignActive:(state=0)
スリープから復帰
applicationDidBecomeActive:(state=0)

[3C] 電話が掛かってきた

applicationWillResignActive:(state=0)
電話断
applicationDidBecomeActive:(state=0)

[3B] 他アプリでメモリを大量利用

com.apple.launchd[1] (UIKitApplication:com.yourcompany.
ApplicationDelegateSample[0x8bb3][3919]) <Notice>:
 (UIKitApplication:com.yourcompany.ApplicationDelegateSample
[0x8bb3]) Exited: Killed

SpringBoard[28] <Warning>: Application 'ApplicationDelegateSample'
 exited abnormally with signal 9: Killed

その後起動
application:didFinishLaunchingWithOptions:
applicationDidBecomeActive:(state=0)


わかったこと


  • メモリ不足になると KILLされる。その時に UIApplicationDelegateは呼ばれない。


参考情報

iOS4 アプリケーションの状態遷移 - yagutaの日記
状態遷移図にまとめられていて役に立つ。状態一覧と各状態の説明などが詳しい。
マルチタスキング対応後のアプリのイベントフローをまとめてみた - A Day In The Life
メモリ警告メソッドの呼び出しタイミングなどが詳しい。
空きメモリが少ない時に、iPhoneの中で何が起きるのか、また調べてみました。 - The iPhone Development Playground
実機で試した記録など。今回の検証と一致。
Togetter - 「iOS4でマルチタスキングを実現するときの注意点まとめ」
いろいろ

- - - -
次回は UIApplicationExitsOnSuspend==YES のケース。



(2010-09-14追加) GitHub
ApplicationDelegateSample at 2010-09-02 from xcatsan's iOS-Sample-Code - GitHub

機種名称を取得する

2010年8月30日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

前回は UIDevice を使って各種情報を取得したが機種名称は取れなかった。

Cocoaの日々: UIDevice

例えば UIDevice.model から "iPhone" を取得できるが 3GS なのか 3G なのかがわからない。どうやったら機種名が取れるのか?


機種名称取得


機種名称取得の方法を探してみるといくつかみつかった。

Determine Device Type – 3G or 3GS / iPod First or Second Generation

Determine device (iPhone, iPod Touch) with iPhone SDK - Stack Overflow


特別な API は用意されていないので、どちらも sysctlbyname 経由で hw.machine 情報を取り出し、それを元に機種名に変換している。こんな感じ(擬似コード)
sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *platform = [NSString stringWithCString:machine];
    if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
    if ([platform isEqualToString:@"iPhone1,2"]) return @"iPhone 3G";
     :


サンプル


サンプルを作ってみた。こんな感じ。


ちゃんと機種名が取れた。

ソースコードは GitHub からどうぞ。
DeviceTypeSample at 2010-08-30b from xcatsan's iOS-Sample-Code - GitHub

コードはこんな感じ。DeviceInfoUtilityというクラスを用意した。
@interface DeviceInfoUtility : NSObject {

}
+ (NSString *) hwMachine;
+ (NSString *) deviceName;

@end

まず hw.machine を取得する箇所。これは参考サイトからコピペした。
+ (NSString *) hwMachine{
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *platform = [NSString stringWithCString:machine
           encoding:NSUTF8StringEncoding];
    free(machine);
    return platform;
}

機種名取得箇所。機種名称は plist に記述してそちらを参照するように変更した。機種名はどうせ今後増えるので。
+ (NSString *) deviceName {
 NSString* hwMachine = [self hwMachine];
 NSString* filePath = [[NSBundle mainBundle]
        pathForResource:@"DeviceNameList" ofType:@"plist"];
 NSDictionary* deviceList = [NSDictionary dictionaryWithContentsOfFile:filePath];
 NSString* deviceName = [deviceList objectForKey:hwMachine];

 if (deviceName == nil) {
  deviceName = hwMachine;
 }
 return deviceName;
}

該当する機種名称が存在しない場合は hw.machine の値をそのまま返すようにしている。こうしておけば新機種がでた場合でも最低限区別ができるようにはなる。

plistはこんな感じ。

UIDevice

2010年8月29日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UIDevice を使うとOSのバージョンやバッテリー情報が取得できる。

UIDevice Class Reference

サンプルプログラムを作って確認してみた。UIDevice経由で取得できる情報はこんな感じ。


batteryLevelは batteryMonitoringEnabled=YESとしないと正しい値が取得できない。またNSNotificationCenterへ登録しておけば変化を検出することができる。

proximityState(近接センサー状態)も同様に proximityMonitoringEnabled=YESにする必要があり、こちらも通知を受け取ることができる。


サンプルは GitHub からどうぞ。
UIDeviceSample at 2010-08-29 from xcatsan's iOS-Sample-Code - GitHub

Object with +0 retain counts returned to caller where a +1 (owning) retain count is expected

2010年8月28日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

copyWithZone: を実装して Build And Analyze を掛けたところ下記の指摘を受けた。

Object with +0 retain counts returned to caller where a +1 (owning)
retain count is expected


原因は copyWithZone: で返すオブジェクトが autorelease となっていたため。copyWithZone: の戻りは作法として retainCount==1 として返す必要がある(呼び出し側がオーナーシップを持つ)。

NSCopying Protocol Reference
上記より抜粋:
The returned object is implicitly retained by the sender, who is responsible for releasing it.


それはそれでいいのだが、いじっているとこんな画面が出てきた。ほー。


こんな親切な説明が出てくるんだ。知らなかった。
(それだけ)

オーディオフォーマット変換 afconvert

2010年8月27日金曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

先日 MP3から AIFFへの変換に iTunesを使う方法を紹介したが、MacOSX にはオーディオフォーマットを変換できる afconvert というツールが標準で付属していることがブログのコメントへの書き込みで知った(335gさん情報どうも)。
Cocoaの日々: 効果音販売サイトの紹介と iTunesを使ったMP3からAIFFへの変換方法

調べてみるとなかなか使えそうなツールのようだ。以下、参考になったサイト。

[iPhone] オーディオファイルのフォーマット変換方法 | Sun Limited Mt.

Mac OS Xはコマンドも充実--「afconvert」でオーディオフォーマットを変換 - builder by ZDNet Japan

Happy Happy iPhone Game Programming : .WAVファイルを.CAFファイルにコンバート

.wavファイルをまとめてIMA4圧縮の.cafファイルに変換 - iPhoneアプリ開発ブログ

試してみた。


afconvert


利用例
$ afconvert -f caff -d ima4 -d LEI16@22050 input.mp3 output.caf

-h で使い方、 -hf でフォーマット一覧を見ることができる。
$ afconvert -hf
Audio file and data formats:
    '3gpp' = 3GP Audio (.3gp)
               data_formats: 'aac ' 'samr' 
    '3gp2' = 3GPP-2 Audio (.3g2)
               data_formats: 'aac ' 'samr' 
    'adts' = AAC ADTS (.aac, .adts)
               data_formats: 'aac ' 'aach' 
    'ac-3' = AC3 (.ac3)
               data_formats: 'ac-3' 
    'AIFC' = AIFC (.aifc, .aiff, .aif)
               data_formats: I8 BEI16 BEI24 BEI32 BEF32 BEF64 UI8 'ulaw' 
                             'alaw' 'MAC3' 'MAC6' 'ima4' 'QDMC' 'QDM2' 
                             'Qclp' 'agsm' 
    'AIFF' = AIFF (.aiff, .aif)
               data_formats: I8 BEI16 BEI24 BEI32 
    'amrf' = AMR (.amr)
               data_formats: 'samr' 
    'caff' = Apple CAF (.caf)
               data_formats: '.mp1' '.mp2' '.mp3' 'QDM2' 'QDMC' 'Qclp' 
                             'Qclq' 'aac ' 'aach' 'aacl' 'alac' 'alaw' 
                             'dvi8' 'ilbc' 'ima4' I8 BEI16 BEI24 BEI32 
                             BEF32 BEF64 LEI16 LEI24 LEI32 LEF32 LEF64 
                             'ms\x00\x02' 'ms\x00\x11' 'ms\x001' 'samr' 
                             'ulaw' 
    'm4af' = Apple MPEG-4 Audio (.m4a)
               data_formats: 'aac ' 'aach' 'aacl' 'alac' 
    'MPG1' = MPEG Layer 1 (.mp1, .mpeg, .mpa)
               data_formats: '.mp1' 
    'MPG2' = MPEG Layer 2 (.mp2, .mpeg, .mpa)
               data_formats: '.mp2' 
    'MPG3' = MPEG Layer 3 (.mp3, .mpeg, .mpa)
               data_formats: '.mp3' 
    'mp4f' = MPEG-4 Audio (.mp4)
               data_formats: 'aac ' 'aach' 'aacl' 
    'NeXT' = NeXT/Sun (.snd, .au)
               data_formats: I8 BEI16 BEI24 BEI32 BEF32 BEF64 'ulaw' 
    'Sd2f' = Sound Designer II (.sd2)
               data_formats: I8 BEI16 BEI24 BEI32 
    'WAVE' = WAVE (.wav)
               data_formats: UI8 LEI16 LEI24 LEI32 LEF32 LEF64 'ulaw' 
                             'alaw' 


変換結果


手元にあった 1秒のMP3形式の効果音ファイルを CAF, AIFF へ変換してみた。
508  afconvert -f caff -d ima4 -d LEI16@22050 -c 1-o pin1.caf pin.mp3
$ afconvert -f caff -d ima4 -d LEI16 MP3_original.mp3 CAFF_LE16.caf
$ afconvert -f caff -d ima4 -d LEI16@22050 MP3_original.mp3 CAFF_LE16.caf
$ afconvert -f AIFF -d BEI16 MP3_original.mp3 AIFF_BEI16.aif

AIFF はWikipediaによればビッグエンディアン固定のようなので "-f AIFF -d LEI16" と指定するとエラーとなる。
$ afconvert -f AIFF -d LEI16 MP3_original.mp3 AIFF_LEI16.aif
Error: ExtAudioFileCreateWithURL failed ('fmt?')

ファイルサイズは次の通り。

AIFF_BEI16.aif       167,868
AIFF_BEI16@22050.aif  85,984
CAFF_LEI16.caf       167,868
CAFF_LEI16@22050.caf  85,984
AIFF_itunes.aif      159,206
MP3_original.mp3      22,569

MP3_original.mp3 が変換元、AIFF_itunes.aif はiTunes10で変換したもの。面白いことに AIFF も CAFF もサイズは同じとなった。また当然ながらビットレートを制限 (22KHz)に制限した邦画サイズが小さくなる(44KHzの半分なので、サイズも約半分)。


サンプル


これらの音をサンプルを作って実機で鳴らしてみた(iPhone3GS/iOS4.01)

使用した効果音は下記から拝借した。
▼こだま標準効果音ライブラリ(β) |株式会社こだまプロダクション
(このサイトでは数多くの音源を紹介していて「無料のコンテンツへの利用、かつ個人でのご利用」であれば条件付きで無料となる。商用利用向けのライセンスもありそんなに高くない)

結果:
・どれも音の違いは感じられなかった。=> 22KHzでも十分なのでサイズを気にする場合はレートを低くする
・MP3 は実機では再生できなかった。シミュレータでは鳴る(これはネットで出回っていた情報の通り)。
・AIFF=ビッグエンディアンでも再生ができた(ネット上ではリトルエンディアンで無いと駄目みたいな情報もあった)


サンプルのソースコードは GitHub からどうぞ:
SoundSamples at 2010-08-27 from xcatsan's iOS-Sample-Code - GitHub


AIFF vs CAF


AIFF(Audio Interchange File Format) は Wikipedia によれば、
AIFF (Audio Interchange File Format、読みは後述) は、アップルコンピュータにより開発された音声データのファイルフォーマットである。主としてMacintoshやAmiga上で使われるファイル形式である。
とあった。通常は非圧縮でビッグエンディアン。

AIFF - Wikipedia


一方、CAF(Core Audio Format) とは Core Audio APIs でサポートされるファイルフォーマットのようだ。
Apple Core Audio Format Specification 1.0: CAF File Overview

上記情報によれば次の特徴がある。

  • サイズの制約なし(数百年分保持できる!?)
  • ヘッダのファイナライズが不要(音データの追記が可能)
  • 多くのでデータフォーマットに対応
  • 多くの補助データ(メタデータ / auxiliary data)に対応(annotations, markers, channel layouts, ...)

この CAF はコンテナとも言うべき存在で、中身のデータ形式(データフォーマット・圧縮方式など)は様々なものに対応している。

Core Audio Overview: Supported Audio File and Data Formats in Mac OS X

上記ページから抜粋:
'.mp3', 'MAC3', 'MAC6', 'QDM2', 'QDMC', 'Qclp', 'Qclq', 'aac ', 'agsm', 'alac', 'alaw', 'drms', 'dvi ', 'ima4', 'lpc ', BEI8, BEI16, BEI24, BEI32, BEF32, BEF64, LEI16, LEI24, LEI32, LEF32, LEF64, 'ms\x00\x02', 'ms\x00\x11', 'ms\x001', 'ms\x00U', 'ms \x00', 'samr', 'ulaw'


CAF は iPhoneの場合、電話着信音のフォーマットとしても利用されている。

iOS で効果音を鳴らす場合は、AIFF と CAF のどちらを使うべきか?ネットを探したところフォーマットの違いはわかったものの、使い分けの指針らしきものは見つけられなかった(知っている方がいたら是非情報を教えて下さい)。


その他


フリーの効果音サイト(らしい)。
"The Freesound Project"
freesound :: home page

ファイル書き出し

2010年8月26日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

覚え書き。CSVファイルを書き出した時のメモ。

ファイル書き出し


NSFileHandle が使える。

1. NSFileHandleを取得
2. NSFileHandleを使ってデータ書き出し
3. NSFileHandleをクローズ


実装例


エラー処理はお好みで。
NSFileManager* fileManager = [NSFileManager defaultManager];

// 既存チェック
if (![fileManager fileExistsAtPath:filePath]) {

 // 新規の場合は空のファイルを作成
 [fileManager createFileAtPath:filePath
        contents:[NSData data]
      attributes:nil];
}

NSFileHandle* fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];

for (NSString* row in CSVArray) {
 NSData* data = [row dataUsingEncoding:NSShiftJISStringEncoding];
 [fileHandle writeData:data];
}
[fileHandle closeFile];

効果音を鳴らす

2010年8月25日水曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

覚え書き。iPhoneで効果音を鳴らす方法など。

この辺りが参考になった。
AudioService - 短いファイルを再生する - iPhoneアプリケーション開発
[iPhone 開発メモ] 短い音を再生する方法 | Sun Limited Mt.


効果音を鳴らす


ファイル形式:AIFF

実装のポイントは3つ。

1. AudioToolbox.framework を追加
2. SystemSoundIDを登録(交換音ファイルを登録)
3. 音を鳴らす


こんな感じ。できるだけ使い慣れているNS系クラスで書いている。

効果音ファイル:sound.aif (AIFF形式)
SystemSoundID soundID;
NSURL* soundURL = [[NSBundle mainBundle] URLForResource:@"sound"
       withExtension:@"aif"];
AudioServicesCreateSystemSoundID ((CFURLRef)soundURL, &soundID);
[soundURL release];
AudioServicesPlaySystemSound (soundID);

CSVの改行コードなど

2010年8月24日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

CSVファイルを書き出すことがあったので、その覚えがき。

※Mac用オフィス 2004 と Windows用オフィス2007 で確認。

(勝手に)CSV仕様


・基本はデータを " で囲う。(例)"abc","123", ...

・エスケープ文字(¥)が出現する場合はそれ自体をエスケープする(例)¥→¥¥
・ " が出現する場合は2つ重ねる。(例)"NEKO" なら ""NEKO"" とする

・行末の改行コード
 Windows: ¥r¥n
 MacOSX: ¥r

・データ内に改行が入る場合、改行コードはエスケープコードを使う。
 Windows: ¥n
 MacOSX: ¥r

 (例)"ABC¥r¥nDEF"

  ※なお、Windowsの改行に合わせた場合、MacOSXのオフィスでは改行されない(その逆も同じ)


CSVのコードを調べたい場合は適当なデータをエクセルで作り、それをCSVで吐き出してバイナリを調べればよい。

MD5変換

2010年8月23日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

文字列を MD5変換したい。下記が参考になった。
MD5 algorithm in Objective C - Stack Overflow


サンプルコード


上記のサイトを参考に(というかほとんどそのまんまだが)NSString のカテゴリを書いてみた。

#import "NSString_Extension.h"
#import 

@implementation NSString (Extension)

/*
 * from: http://stackoverflow.com/questions/1524604/md5-algorithm-in-objective-c
 */
- (NSString *)md5String
{
    const char *cStr = [self UTF8String];
    unsigned char result[16];
    CC_MD5( cStr, strlen(cStr), result ); // This is the md5 call
    return [NSString stringWithFormat:
   @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
   result[0], result[1], result[2], result[3], 
   result[4], result[5], result[6], result[7],
   result[8], result[9], result[10], result[11],
   result[12], result[13], result[14], result[15]
   ];  
}

@end
なかなか便利。

ziparchive を使って ZIP圧縮する(旧:Objective-Zipを使って ZIP圧縮する)

2010年8月22日日曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

2010-12-19 [訂正] Objective-Zip ではなく ziparchive の間違いです。
Source Checkout - ziparchive - Project Hosting on Google Code
根本的な間違いで失礼しました。


iPhoneアプリで ZIP圧縮をかけたい。探したところ Objective-Zipziparchive なるものが見つかったので使ってみた。
objective-zip - Project Hosting on Google Code
Source Checkout - ziparchive - Project Hosting on Google Code

情報など


下記のサイトが参考になった。
Compressing directory contents on the iPhone with zlib :: Post No Bills


実装など


※今回はサンプルなし

こんな感じで使う。

#import "ZipArchive.h"
 :
- (void)someMethod
{
 NSString* zipFile = @"/some/path/target.zip";
 NSArray* fileList = [NSArray arrayWithObjects:
  @"/some/data/file1.csv", @"/some/data/file2.csv", nil];

 // (1) 準備
 ZipArchive* archiver = [[ZipArchive alloc] init];
 [archiver CreateZipFile2:zipFile];

 // (2) ファイル追加(圧縮)
 for (NSString* filePath in fileList) {
  [archiver addFileToZip:filePath
          newname:[filePath lastPathComponent]];
 }
 [archiver CloseZipFile2];
 [archiver release];
}
addFileToZip:newname: の最初に引数に追加したいファイルをパス付きで指定する。2番目の引数はZIPファイル内のパス情報。例の場合、[filePath lastPathComponent] すなわちファイル名だけを渡している(file1.csv, file2.csv)。この場合、圧縮されたファイルを解凍すると次のようになる。
target.zip
 ↓解凍
 target/
   |-- file1.csv
   |-- file2.csv
トップレベルのフォルダ名は ZIPファイルの拡張子を除いた部分が使われる。

もし2番目の引数(newname:)にパス情報のついた filePathをそのまま渡すと、ZIPファイル内にはパス階層も保存される。解凍すると次のようになる。
target.zip
 ↓解凍
 some/
   |--data/
       |-- file1.csv
       |-- file2.csv

なお利用するにはフレームワークへ lib.1.2.3.dylib を追加しておく。

効果音販売サイトの紹介と iTunesを使ったMP3からAIFFへの変換方法

2010年8月21日土曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

soundsnap


知人から教えてもらった効果音販売サイト。
Soundsnap.com: High Quality Sound Effects and Loops

ここは量・質共になかなかいい。1秒からの短い効果音も多いので iPhoneアプリでちょっとした音を出したい(例えばボタンのタップ音)時に使える音が揃っている。

カテゴリは 16種類。


一覧の各効果音毎に Flash製のプレイヤーが付いていてその場で音を確認することができる。グラフがあるのもいい。


費用は 5ファイル $9 より。購入数が増えると単価が安くなる。

ファイルの形式は MP3 と WAV。AIFFは記載はあるものの、ほとんどの音源で提供されていない。


MP3をAIFFに変換する


iPhone で効果音を鳴らす場合、MP3ではなく AIFF を使う。このため効果音を MP3で入手した場合は AIFFに一旦変換する必要がある。この変換は iTunes で行うことができる。以下、iTunes 9.2.1 でのやり方。

変換元


今回はこのMP3ファイルをAIFFへ変換してみる。

(1)iTunesへ追加


MP3ファイルを iTunesへ読み込ませる。これは MP3ファイルを iTunesへドラッグ&ドロップすればいい。


(2)設定変更


iTunesの「環境設定」を開く。


「一般」の中にある「読み込み設定...」ボタンを押す。


「読み込み方法」を "AIFFエンコーダ" に変更する。

※なおこの変換作業が終わったら元に戻しておくこと(通常は AAC エンコーダ)。そうでないとオーディオCDを読み込むときに AIFF形式でファイルが作られる(サイズがバカでかくなる)。

設定が終わったら、設定画面をすべて閉じる。


(3)AIFFファイル作成


MP3ファイルを選択した状態でメニュー「詳細」から "AIFF バージョンを作成" を選択する。


すると MP3ファイルと同じフォルダ内に AIFF形式のファイルが作成される。


ちなみにサイズは次のようになった。
check.mp3 (8KB)
check.aiff (49KB)

画像を横に並べたスクロールビューアの作成 [5] 自動スクロール

2010年8月20日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回まで]
Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [3] 循環スクロール(無限スクロール?)
Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [4] 実機確認(修正)

今回は自動スクロールを実装する。


自動スクロール


自動スクロールとはユーザの操作を待たずに勝手にスクロールすること。
02

ユーザが指でドラッグすると自動スクロールは一旦中止され、ドラッグが終わってからしばらくすると再開する。


実装


自動スクロールさせるには NSTimer を使って一定間隔で UIScrollView をスクロールさせればよい。UIScrollView の contentOffset プロパティを変更するとスクロールが発生するのでこれを NSTimer発火時に変更する。発火間隔は 0.05秒とした。この位が見た目ちょうどいい。
- (void)timerDidFire:(NSTimer*)timer
{
 if (autoScrollStopped_) {
  return;
 }
 
 CGPoint p = self.scrollView.contentOffset;
 p.x = p.x + 1;
 
 if (p.x < IMAGE_WIDTH * MAX_SCROLLABLE_IMAGES) {
  self.scrollView.contentOffset = p;
 }
}
autoScrollStopped_ は BOOL型の変数でユーザが操作を行っている時には YES が入る。これによってタイマーが発火しても自動スクロールは発生しない。なお UIScrollView.contentSize は有限なので境界値チェックをいれている。これによって右端まで来た場合は自動スクロールは停止する。自動スクロールのメインとなる処理はこれだけ。 他には、ユーザの操作によって autoScrollStopped_ を YES/NO で切り替える処理がある。これは UISCrollViewDelegate のメソッドを使う。

まずユーザが指でドラッグを開始した場合の処理
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
 [self stopAutoScroll];
}

- (void)stopAutoScroll
{
 autoScrollStopped_ = YES;
}
そしてドラッグが終わった時の処理。これは2つ必要。
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
 [self restartAutoScrollAfterDelay];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
 if (!decelerate) {
  [self restartAutoScrollAfterDelay];
 }
}
scrollViewDidEndDecelerating だけでは不足で、scrollViewDidEndDragging:willDecelerate: も必要。

再開処理は一手間掛けてある。
- (void)restartAutoScroll
{
 autoScrollStopped_ = NO;
}

- (void)restartAutoScrollAfterDelay
{
 [self performSelector:@selector(restartAutoScroll)
      withObject:nil
      afterDelay:AUTO_SCROLL_DELAY];
}
ユーザ操作完了後すぐに自動スクロールが発生するとせわしない?感じがするので若干の遅れ(AUTO_SCROLL_DELAY=3秒)を持たせている。


UISCrollView のドラッグとスクロールの終わり


UIScrollView を指でドラッグして離す(フリック?)と、しばらくの間慣性スクロールが続きやがて速度を落として止まる。この止まったタイミングで scrollViewDidEndDecelerating: が呼び出される。このタイミングで自動スクロールを再開させればいいのだが、実際に触ってみると自動スクロールが再開しないケースがあることがわかった。それはドラッグした指をフリックせずに(弾かずに)ピタっと止めた場合。この場合は scrollViewDidEndDecelerating が呼び出されない。このメソッドはその名の通り「速度を落とす」(Decelerate)動作が終了した時だけ呼ばれて、このようにピタっと止めた場合には呼ばれない。

このピタっと止めた場合のイベントを拾うのに同じ UIScrollViewDelegate の scrollViewDidEndDragging:willDecelerate: を使う。このメソッドはスクロールの終わりではなく、ユーザのドラッグ操作の終わりを検出する。2番目の引数 decelerate はドラッグ終了時に慣性スクロールが働いているかどうかを表している。decelerate == YES の場合は、このメソッドが呼び出された時もまだスクロールが終わっていない。指でピタっと止めたときに呼び出された時には decelerate == NO で呼び出される。 まとめると次の通り。

(a) フリックしてスクロール
  ドラッグ
    ↓
  scrollViewWillBeginDragging
    ↓
  離す(フリック)
    |
    |慣性スクロール
    ↓
  scrollViewDidEndDragging:willDecelerate: (decelerate==YES)
    |
    |減速
    |
    |スクロール停止
    ↓
  scrollViewDidEndDecelerating:
(b) ドラッグしてピタっと止める
  ドラッグ
    ↓
  scrollViewWillBeginDragging
    ↓
  ピタっと止める
    ↓
  scrollViewDidEndDragging:willDecelerate: (decelerate==NO)
上記動作により自動スクロール再開の為に2つのメソッドをチェックしている。(a)パターンではスクロール終了前に scrollViewDidEndDragging:willDecelerate:(YES) が呼び出されるので、その場合には自動スクロールの再開はさせないようにしている。このパターンの場合はその後に続く scrollViewDidEndDecelerating: で再開させている。


サンプル


見た目は前回と変わらないので割愛。

ソースは GitHub からどうぞ。
CirculationScroll at 2010-08-20 from xcatsan's iOS-Sample-Code - GitHub




画像を横に並べたスクロールビューアの作成 [4] 実機確認(修正)

2010年8月19日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [3] 循環スクロール(無限スクロール?)

念のため実機で確認したところ、素早くスクロールさせると何も表示されず真っ黒になることがわかった。

原因はこれ。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
 CGFloat position = scrollView.contentOffset.x / IMAGE_WIDTH;
 CGFloat delta = position - (CGFloat)leftImageIndex_;
 
 if (fabs(delta) >= 1.0f) {
  if (delta > 0) {
   [self scrollWithDirection:kScrollDirectionRight];
  } else {
   [self scrollWithDirection:kScrollDirectionLeft];   
  }  
 }
}

スクロールが1画像単位でしか行えないようになっている。素早くスクロールすると delta値が 2以上になることがある。その状態が続くとインデックスを調整する処理が追いつかず、すぐに何も表示されない状態になってしまう。

原因がわかれば対処は簡単。deltaが 2以上になることを前提として、その分だけ内部処理を回すようにする。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
 CGFloat position = scrollView.contentOffset.x / IMAGE_WIDTH;
 CGFloat delta = position - (CGFloat)leftImageIndex_;
 NSInteger count = (NSInteger)fabs(delta);

 for (int i=0; i < count; i++) {
  if (delta > 0) {
   [self scrollWithDirection:kScrollDirectionRight];
  } else {
   [self scrollWithDirection:kScrollDirectionLeft];   
  }  
 }
}

これで素早い操作に追随できないということは無くなった。

画像を横に並べたスクロールビューアの作成 [3] 循環スクロール(無限スクロール?)

2010年8月18日水曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [2] サンプル

前回の続き。今回は循環スクロールを実装する。


循環スクロール


「循環スクロール」とは画像を円環のようにつなげ合わせて、その一部を表示させ、スクロールできることを指す。

イメージ:

左右の端がなくどちらの方向にも延々とスクロールすることができるので無限スクロールと言っていいかもしれない。


実装


前回までの実装をベースに循環スクロールを実装する。ポイントは次の2点。
1. スクロール対象のエリアを大きく取る
2. 画像を循環表示させる

1. は具体的には UIScrollView.contentSize で設定する。今回はこれを非常に大きな数(10,000 x 80ピクセル程度)に設定することで擬似的に無限スクロールできるようにした。
2. は、例えば8枚の画像がある場合、9枚目の表示には1枚目の画像を使うといった処理。

基本的に前回のコードへ上記実装を加えれば循環スクロールが実現できる。実装はここで説明するよりもソースコードを見てもらった方が(言葉よりも)分かりやすいと思うのでそちらを参照のこと。


サンプル


静止画では分かりづらいが、サンプルをビルドして実行すると延々とスクロールできる動作が確認できる。


ソースコードは GitHub からどうぞ。
CirculationScroll at 2010-08-18 from xcatsan's iOS-Sample-Code - GitHub

- - - -

次回はこれに自動スクロールを加える。


2011-01-10 補足説明

ビューの配置イメージはこんな感じ。可視領域の両隣にもViewを配置しておく。
左へスワイプするとスクロールが始まる。右に隠れていたビューが画面に出てくる。
スクロール中は UIScrollViewDelegate の scrollViewDidScroll: が呼ばれる。1画像分の移動したらビューの再配置処理を開始する(CirculationScrollViewController.mの221行あたり)。
まず一番左のビューのX位置を右端へ変更する。ビューは配列で管理しているので、その配列内で左端、右端のビューがどこかを保持しておくために rightViewIndex_, leftViewIndex_ を使う。これらはビューの再配置に伴い変わっていく。
そして画像のリストから次に表示すべき画像を取得して右端のビューに割り当てる。180行あたりの scrollWithDirection: がこの処理を行っている。すべての画像が一巡して表示されているなら、一番最初の画像を右端へ割り当てることになる。こうすると画像のリストが循環してするようにみえる。

上記を左にしろ右にしろ繰り返すことで最終的に循環スクロールが完成する。


さらに画像を表示しているViewを格納している配列 viewList の扱いを実際の動きと合わせてみてみる。
viewList は一種の循環配列で、左端ビューの配列内の位置を leftViewIndex_, 右端ビューの配列内位置を rightViewIndex_ で管理している。初期状態は上記のようにわかりやすい対応となっている。
 左にスワイプすると右スクロールが発生。1画像分のスクロールが完了した時点でscrollWithDirection: が呼び出される。右スクロールの場合、左端のビューを表示上は右端へ移動する。leftViewIndex_ で左端のビューを取得した後、frame.origin.x を補正する。そして最後に leftViewIndex_, rightViewIndex_ の位置を補正する。右スクロールなのでそれぞれの値に+1を加える。rightViewIndex_ は配列の最大サイズをオーバーするので、剰余をとって0に戻す。それらが終わると下の状態になる。
表示上、右端にあるビューは実は viewList配列の中では rightViewIndex_ が示す0番目のビューにあたる。


画像を横に並べたスクロールビューアの作成 [2] サンプル

2010年8月17日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: 画像を横に並べたスクロールビューアの作成 [1] アイディア

前回のアイディアを元にしてサンプルを作ってみた。


サンプル

こんな感じ。


8枚の小さな画像を左右にスクロールして見ることができる。

右端(左端)には BLANK と書かれた画像が配置されていて、バウンススクロールさせた時にはそれが(チラッと)表示される。


ソースコードは GitHubからどうぞ。
CirculationScroll at 2010-08-17 from xcatsan's iOS-Sample-Code - GitHub


解説


いくつかポイントがあるが、まずは画像の切り替わり検出について。前回のアイディアで説明したように、左右外側の UIImageView の再配置のタイミングは画像1個分がスクロールしたところで、これを検出する必要がある。
(前回の図より)

これは UIScrollViewDelegate のメソッド scrollViewDidScroll: が使える。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
 CGFloat position = scrollView.contentOffset.x / IMAGE_WIDTH;
 CGFloat delta = position - (CGFloat)leftImageIndex_;
 
 if (fabs(delta) >= 1.0f) {
  if (delta > 0) {
   [self scrollWithDirection:kScrollDirectionRight];
  } else {
   [self scrollWithDirection:kScrollDirectionLeft];   
  }  
 }
}
IMAGE_WIDTH は画像1個の横幅。あらかじめ前回の位置を leftImageIndex_ に保存しておき、そこからのズレが画像1個分( >1.0f)になったかどうかで、画像1個分のスクロールが発生したかどうかを検出する。検出した後、実際のスクロール処理( scrollWithDirection:)を行う。

スクロール処理はこんな感じ。まずヘッダファイル。
@interface CirculationScrollViewController : UIViewController  {

 UIScrollView* scrollView_;
 NSArray* viewList_;
 
 NSArray* imageList_;

 NSInteger leftImageIndex_; // index of imageList

 NSInteger leftViewIndex_; // index of viewList
 NSInteger rightViewIndex_; // index of viewList
}
@property (nonatomic, retain) IBOutlet UIScrollView* scrollView;
@property (nonatomic, retain) NSArray* viewList;
@property (nonatomic, retain) NSArray* imageList;

@end

定義
#define IMAGE_WIDTH   80
#define IMAGE_HEIGHT  80
#define DISPLAY_IMAGE_NUM 4
#define MAX_IMAGE_NUM  (DISPLAY_IMAGE_NUM+2)

スクロール処理の実装
- (void)scrollWithDirection:(ScrollDirection)scrollDirection
{
 NSInteger incremental = 0;
 NSInteger viewIndex = 0;
 NSInteger imageIndex = 0;

 if (scrollDirection == kScrollDirectionLeft) {
  incremental = -1;
  viewIndex = rightViewIndex_;
 } else if (scrollDirection == kScrollDirectionRight) {
  incremental = 1;
  viewIndex = leftViewIndex_;
 }

 // change position
 UIImageView* view = [self.viewList objectAtIndex:viewIndex];
 CGRect frame = view.frame;
 frame.origin.x += IMAGE_WIDTH * MAX_IMAGE_NUM * incremental;
 view.frame = frame;
 
 // change image
 NSInteger numberOfImages = [self.imageList count];
 leftImageIndex_ = leftImageIndex_ + incremental;

 if (scrollDirection == kScrollDirectionLeft) {
  imageIndex = leftImageIndex_ -1;
 } else if (scrollDirection == kScrollDirectionRight) {
  imageIndex = leftImageIndex_ + DISPLAY_IMAGE_NUM;
 }
 if (0 <= imageIndex && imageIndex < numberOfImages) {
  view.image = [self.imageList objectAtIndex:imageIndex];
 } else {
  view.image = [self blankImage];
 }

 // adjust indicies
 leftViewIndex_ = [self addViewIndex:leftViewIndex_ incremental:incremental];
 rightViewIndex_ = [self addViewIndex:rightViewIndex_ incremental:incremental];
}
前回のアイディアに沿った処理を行っているだけ。
1. 左(右)の外側の UIImageView を再配置する
2. UIImageView に新しい画像を設定する
3. インデックスを更新する

各インデックスのイメージは次の通り。


- - - -
インデックスの更新処理がややこしくて少々手こずったがなんとかできた。次は循環スクロールに挑戦する(もともとこれがしたかった)。

画像を横に並べたスクロールビューアの作成 [1] アイディア

2010年8月16日月曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

画像を横に並べて指でスクロールできるちょっとしたビューアを作ってみる。
こんなイメージ。

実装イメージ


実装イメージはこんな感じ。表示したい画像が横1列に並んでいて、それを UIScrollViewで一部だけ表示してスクロールできるようにする。


指で左方向へドラッグすると画像が左へ動きスクロールしているように見える。左端の画像は一部が隠れ、右端に隠れていた画像が少しづつ見えてくる。



実装方法


一番簡単なのは表示する画像を横一列に並べて、その一部だけを UIScrollViewで表示する方法がある。この方法はあらかじめ画像数が分かっていて少数の場合は十分動作する。ただ画像が多い場合はその分メモリを使用するので現実的ではない。このため一工夫が必要となる。

そこで今回は UIImageView を下記のように並べて、スクロールの度に再配置する方法を考えてみた。

画面上に表示可能な画像数が4つだとすると、両側の2つを加えて計6つの UIImageView があれば良い。

これを起点にして実際の動作を考えてみる。まず左へスクロールしてみる。

すると [1] の左側が隠れはじめ、隠れていた[5]の一部がみえはじめる。

続けてスクロールするとやがて [1]は完全に画面の外側に隠れる。この時点をプログラムで検出する。

[1]が隠れたのが検出できたら、一番左側にある [0] を今度は一番右側へ配置しなおす。

最後に表示上の左端を表すマーカー(図上の<LEFT>)を更新する。すると表示4つ、両側2つの最初の形に戻る。


ビューの座標系視点でこのアイディアを確認するとこんな感じ。
左へスクロールするということは UIScrollViewの可視領域が右へ移動するということなので、図のように UIScrollView はだんだんと右へづれて行く。UIViewImageの再配置とは、表示位置(origin)を変更するということに他ならない(例:[0]の座標が最初のスクロールの後、x5に変更される)。


- - - -
このアイディアを元に次回から実装に入っていく。

UITableView - 特定のセクションヘッダの高さを0にする

2010年8月15日日曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

UITableViewDelegate にはセクションヘッダと降ったの高さを変えるためのメソッドが用意されている。

UITableViewDelegate Protocol Reference
– tableView:heightForHeaderInSection:
– tableView:heightForFooterInSection:

特定のセクションの場合 0 を返せば、そのセクションヘッダ/降ったの高さが 0になる...ハズなのだが試したところそうならなかった。動きとしてはこのメソッドで 0を返すと標準の高さになってしまう。試しに 0.1 を返しても駄目だった。

そこでセクションに大きさ0のカスタムビューを設定して、なおかつ高さを 0.1としてみた。
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
 if (section == 0) {
  return zeroSizeView_;  // zeroSizeView_ = [[UIView alloc] initWithFrame:CGRectZero];
 } else {
  return nil;
 }
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
 if (section == 0) {
  return 0.1;
 } else {
  return tableView.sectionHeaderHeight;
 }
}

すると指定セクションのヘッダの高さが0となった。

それだけ。

UITableView のヘッダの高さを変える(その2:アニメーション)

2010年8月14日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

以前、UITableView のヘッダの高さを変えるコードを紹介した。

Cocoaの日々: UITableView のヘッダの高さを変える

この時はヘッダのリサイズが完了した後、テーブル本体の位置が調整されるようになっていた。これをヘッダのリサイズアニメーションと同時にテーブル本体の位置調整をアニメーション付きで行えるようにする。


実現方法


正攻法はないので今回も少しトリッキー。今回はダミーセクションを用意し、これの高さを調整することでテーブル本体のアニメーションを実現する。

イメージ:

1. まず高さ0セルをひとつだけ持つ Section 1 を用意しておく(図の左の状態)。
2. Headerの高さ増加開始
3. Headerの高さが増加すると同時に Section 1 内のセルの高さを増やし、アニメーションさせる(図の右の状態)。

すると Headerが下へ伸びるのと同時に Section2 以下のテーブル本体も下へ移動する。


実装


ポイントは、section 1 (indexPath.section == 0) をダミーセクションとして扱うこと。UITableViewDataSourceのメッソドで section 0 とそれ以外とで処理を分けていく。
- (UITableViewCell *)tableView:(UITableView *)tableView
 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
 if (indexPath.section == 0) {
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"CELL1"];
  
  if (cell == nil) {
   cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
             reuseIdentifier:@"CELL1"] autorelease];
   UIView* view = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
   cell.backgroundView = view;
  }
  return cell;

 } else {
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"CELL2"];
  
  if (cell == nil) {
   cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
             reuseIdentifier:@"CELL2"] autorelease];
  }
  
  cell.textLabel.text = [array_ objectAtIndex:indexPath.row];
  return cell;
 }
}
section == 0 の時には UITableViewCell.backgroundView へ大きさ0のビューを設定している。こうすることで枠や背景の無い高さ0のセルを用意することができる。

もちろんデリゲートで高さも調整する。
- (CGFloat)tableView:(UITableView *)tableView
 heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
 if (indexPath.section == 0) {
  return spaceCellHeight_;
 } else {
  return tableView.rowHeight;
 }
}

メンバ変数 spaceCellHeight_ は Headerアニメーション時に調整する。
- (IBAction)changeHeader:(id)sender
{
 CGRect frame = self.headerView.frame;

 if (headerOpened_) {
  frame.size.height = 50.0;
  spaceCellHeight_ = 0.0;
 } else {
  frame.size.height = 100.0;
  spaceCellHeight_ = 50.0;
 }
 NSIndexPath* path = [NSIndexPath indexPathForRow:0 inSection:0];
 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:path]
        withRowAnimation:YES];
 [UIView animateWithDuration:0.25
      animations:^{
       self.tableView.tableHeaderView.frame = frame;
      }
      completion:^(BOOL finished){
//       self.tableView.tableHeaderView = nil;
//       self.tableView.tableHeaderView = self.headerView;      }];

 headerOpened_ = !headerOpened_;
 
}
ポイントは reloadRowsAtIndexPaths:withRowAnimation:
これを呼びだすと特定のセルの再描画が行われる。この前後でセルの高さが異なると、その変更過程がアニメーションとなる。なおダミーセクション(セル)を使うことで、前回のように UITableView.headerView へ一時的に nil を入れる必要がなくなった(コメントアウトしてある箇所)。


サンプル


静止図だとちょっと分かりにくいが実行時のスクリーンショットを並べておく。
初期状態。


アニメーション開始。前回と比べると Headerがテーブルへかぶらず、全体的に下に移動しているのがわかる。


アニメーション完了。


ソースコードは GitHub からどうぞ。
TableHeader at 2010-08-14 from xcatsan's iOS-Sample-Code - GitHub


その他


この方法を取る場合の制約がいくつかあるので書いておく。

1. アニメーション速度が固定
iOS標準のアニメーションの長さは大抵 0.2秒間で終わるようになっている。今回テーブル本体のアニメーションはこれに従うため、 Headerのアニメーションの長さもそれと同じ 0.2秒に合わせる必要がある。なお処理上の遅延を考慮すると 0.25秒ぐらいが適当のようだ。

2. Headerの表示領域の高さが実体の高さと一致しない
開閉で Header の高さが異なるが、UITableViewにはそれを教えていない。現状は特に問題ないようだが、今後の iOSバージョンアップで問題が出る可能性がなくもない。

ネットワーク接続状況を知る

2010年8月13日金曜日 | Published in | 4 コメント

このエントリーをはてなブックマークに追加

2010-12-13 続きのブログを書きました。そちらも合わせて読んでみてください。
Cocoaの日々: ネットワーク接続状況を知る[2] SCNetworkReachabilityGetFlags はブロックする

ネットワーク接続状況を知るための API等を調べてみた。前半はアップル提供のサンプルコードについて、後半は自作サンプルについて(ソースコードは GitHubで公開)。

アップル提供のサンプルコード Reachability


SystemConfiguration フレームワークを使うとネットワークの接続状況(WiFi利用または3G回線利用か、接続不可か)を知ることができる。

iOS Reference Library に Reachability というサンプルがある。

Reachability: Classes/Reachability.m

WiFi接続時


3G使用時

フライトモード時
この程度の情報が取れることがわかる。

利用は、まず SCNetworkReachabilityRef を取得する。
SCNetworkReachabilityRef reachability =
 SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]);
用途に応じて何種類かの関数が用意されている。
SCNetworkReachabilityCreateWithAddress
SCNetworkReachabilityCreateWithAddressPair
SCNetworkReachabilityCreateWithName

次に取得した SCNetworkReachabilityRef を SCNetworkReachabilityGetFlags()へ渡して SCNetworkReachabilityFlags を得る。この値はビットで接続状況を表している。
enum {
   kSCNetworkReachabilityFlagsTransientConnection = 1<<0,
   kSCNetworkReachabilityFlagsReachable = 1<<1,
   kSCNetworkReachabilityFlagsConnectionRequired = 1<<2,
   kSCNetworkReachabilityFlagsConnectionOnTraffic = 1<<3,
   kSCNetworkReachabilityFlagsInterventionRequired = 1<<4,
   kSCNetworkReachabilityFlagsConnectionOnDemand = 1<<5,
   kSCNetworkReachabilityFlagsIsLocalAddress = 1<<16,
   kSCNetworkReachabilityFlagsIsDirect = 1<<17,
   kSCNetworkReachabilityFlagsIsWWAN = 1<<18,
   kSCNetworkReachabilityFlagsConnectionAutomatic
    = kSCNetworkReachabilityFlagsConnectionOnTraffic
};
typedef    uint32_t    SCNetworkReachabilityFlags;
これらのビットをチェックすることで 3G経由、WiFi経由、非接続の判断ができる。


自作サンプル


Apple提供のサンプルは若干複雑なので、簡略化したサンプルを作ってみる。サンプル作成にあたっては下記が参考になった。 How to check for local Wi-Fi (not just cellular connection) using iPhone SDK? - Stack Overflow これを参考に簡単なサンプルを作ってみた。NetworkReachability というクラスを用意し、このクラスに接続状況のチェックをさせる。
#import <Foundation/Foundation.h>
#import <SystemConfiguration/SystemConfiguration.h>

enum {
 kNetworkReachableNon = 0,
 kNetworkReachableWiFi,
 kNetworkReachableWWAN
};

#define NetworkReachabilityChangedNotification
 @"NetworkReachabilityChangedNotification"

@interface NetworkReachability : NSObject {

 SCNetworkReachabilityRef reachability_;
}

- (id)initWithHostname:(NSString*)hostname;
- (NSInteger)getConnectionMode;

- (BOOL) startNotifier;
- (void) stopNotifier;

@end
接続判定箇所。
- (int)getConnectionMode
{
 if (reachability_) {
  SCNetworkReachabilityFlags flags = 0;
  SCNetworkReachabilityGetFlags(reachability_, &flags);
  
  BOOL isReachable = ((flags & kSCNetworkFlagsReachable) != 0);
  BOOL needsConnection = ((flags & kSCNetworkFlagsConnectionRequired) != 0);
  if (isReachable && !needsConnection) {
   if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) {
    return kNetworkReachableWWAN;
   }
   
   if ([self getWiFiIPAddress]) {
    return kNetworkReachableWiFi;
   }
   
  }
 }
 return kNetworkReachableNon;
}
WiFi かどうかの判断箇所。
- (NSString*)getWiFiIPAddress
{
 BOOL success;
 struct ifaddrs * addrs;
 const struct ifaddrs * cursor;
 
 success = getifaddrs(&addrs) == 0;
 if (success) {
  cursor = addrs;
  while (cursor != NULL) {
   if (cursor->ifa_addr->sa_family == AF_INET
    && (cursor->ifa_flags & IFF_LOOPBACK) == 0) {
    NSString *name =
    [NSString stringWithUTF8String:cursor->ifa_name];
    
    if ([name isEqualToString:@"en0"]) { // found the WiFi adapter
     return [NSString stringWithUTF8String:
       inet_ntoa(((struct sockaddr_in *)cursor->ifa_addr)->sin_addr)];
       NSString* addressString = [NSString stringWithUTF8String:
       inet_ntoa(((struct sockaddr_in *)cursor->ifa_addr)->sin_addr)];
      freeifaddrs(addrs);
      return addressString;
    }
   }
   
   cursor = cursor->ifa_next;
  }
  freeifaddrs(addrs);
 }
 return NULL;
}
コードはほぼ下記から拝借した。 Matt Brown - Journal of a Software Engineer: How to get the IP address of an iPhone OS v2.2.1 シミュレータ上で実行した場合、WiFiに割り当てられているネットワークが必ずしも en0 ではない(大抵は en1)ので、接続なし表示になることがある。 接続状況に変化があった場合は Notificationをポストする。これはアップルのサンプルから拝借した。もともと用意されている SCNetworkReachabilitySetCallback() を使い Notificationポストへ変換している。
static void ReachabilityCallback(
 SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info)
{
 NSAutoreleasePool* myPool = [[NSAutoreleasePool alloc] init];
 NetworkReachability* noteObject = (NetworkReachability*)info;
 [[NSNotificationCenter defaultCenter]
  postNotificationName:NetworkReachabilityChangedNotification object:noteObject];
 
 [myPool release];
}

- (BOOL)startNotifier
{
 BOOL ret = NO;
 SCNetworkReachabilityContext context = {0, self, NULL, NULL, NULL};
 if(SCNetworkReachabilitySetCallback(reachability_, ReachabilityCallback, &context))
 {
  if(SCNetworkReachabilityScheduleWithRunLoop(
             reachability_, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode))
  {
   ret = YES;
  }
 }
 return ret;
} 

- (void) stopNotifier
{
 if(reachability_!= NULL)
 {
  SCNetworkReachabilityUnscheduleFromRunLoop(
   reachability_, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
 }
}
実行するとこんな感じ。


シミュレータを使って状態の変化を知るには実行後に Macの Wifiをオフにする。 Notificationがポストされて表示が切り替わる。実機の場合は、設定画面を開き WiFiを切ったり、フライトモードにしたりすると確認できる。ただしバックグラウンド実行に準拠していないので Notificationが受け取れるのはサンプルアプリを再び起動した時のみ。また検出までは数秒かかる。

- - - -
ソースコードは GitHub からどうぞ。
xcatsan's iOS-Sample-Code at 2010-08-13 - GitHub

2010-10-21 訂正

NetworkReachable/Classes at 2010-08-13b from xcatsan's iOS-Sample-Code - GitHub

2010-11-14 訂正 

NetworkReachable/Classes at 2010-08-13c from xcatsan's iOS-Sample-Code - GitHub

(差分)
xcatsan's iOS-Sample-Code at 2010-08-13c - GitHub

参考

SCNetworkReachability Reference
SCNetworkReachabilityのリファレンス

人気の投稿(過去 30日間)