Hatena::Groupiphone-dev

iOS プログラミングメモ

2013-09-02 (Mon)

西暦ではなく和暦を使用してる場合にNSDateComponentsのyearが返す値の衝撃 14:44  西暦ではなく和暦を使用してる場合にNSDateComponentsのyearが返す値の衝撃 - iOS プログラミングメモ を含むブックマーク

和暦*1とか使ってないから気付かなかったけど、和暦環境限定の不具合を出してしまいました。

拙作の 記念日リマインダー というアプリでは、特定の日をベースにして1年毎のNSDateを取得して諸々やってる部分があります。日付を計算するのにNSDateComponentsを使うと便利でして、その辺りのコードが以下のような感じになっています。

NSCalendar *calendar = [NSCalendar currentCalendar];

NSDateComponents *components =
   [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:baseDate];

for(int i = 1; i <= 80; i++) {
   components.year ++;
   anniversaryDate = [calendar dateFromComponents:components];

baseDateから80年分の日付オブジェクトを生成してるんですね。NSYearCalendarUnit,NSMonthCalendarUnit,NSDayCalendarUnitの3つを指定してるので、年月日の年の部分だけを変更したモノが得られます。「時分秒」は指定してないので、baseDateの時刻がいくつであってもdateFromComponentsが返すNSDateは00時00分00秒になります。

西暦の場合はこれで何の問題もないのですが、和暦を使ってる場合は元号の値も固定しないと意図した通りに動かない可能性があります。たとえばbaseDateを昭和54年にした場合でもdateFromComponentsが返す時は元号が平成になるようなので、year++すると平成55年にジャンプします。これを解決するには、NSDateComponentsにNSEraCalendarUnitを追加しておくだけです。

NSDateComponents *components =
   [calendar components:NSEraCalendarUnit | NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:baseDate];

これで元号もbaseDateのものが使われるようになるので、晴れて問題は解決となりました。前々からeraってなんだろうなと思ってたんですけど、元号のことだったんですね。まったく、わかってみればなんてことはないけど、ぜんっぜん気付きませんでした。

追記

岸川さんが教えてくれたんですが、NSCalendarにlocaleを指定すれば西暦のままで計算できるようです。

最初の例を直すと、以下のような形でうまく動くようになりました。

NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
calendar.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];

NSDateComponents *components =
   [calendar components:NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit fromDate:baseDate];

for(int i = 1; i <= 80; i++) {
   components.year ++;
   anniversaryDate = [calendar dateFromComponents:components];

components.year = 1900 みたい値を直接代入したい場合は元号がからむと厄介なので、このやり方はいいですね。

*1:設定アプリの「一般>言語環境>カレンダー」で変更可

トラックバック - http://iphone-dev.g.hatena.ne.jp/ktakayama/20130902

2013-08-22 (Thu)

[] Reachabilityが更新されてました 12:17  Reachabilityが更新されてました - iOS プログラミングメモ を含むブックマーク

Reachabilityといえば、ネットワークアクセス状況を監視するためにとりあえず使っとけというサンプルコードですが、実に3年ぶりにARC対応としてアップデートされてたので見てみました。

どうやらNDAな新しいXcodeでビルドしてあるらしくて、Xcode4.6.3だとビルドできません*1。なんということでしょうw そしてXcode4.6.3でビルドするにもMainStoryboardが開けない問題が解決できなくてダメでした。Document Versioning辺りをゴニョゴニョすればいいのかと思ったけど、それだけじゃうまくいきませんでした。Storyboardファイルを開こうとするとクラッシュする。うは。

まあ単純なUIなので、動作テストをするために適当にMainStoryboardを作りなおして試してみました。UIImageViewとUITextFieldとUILabelをいくつか置いて、APLViewControllerとOutletで接続するだけの簡単なお仕事です。これ以外にもSystemConfiguration.frameworkも追加しておおかないといけないので忘れずに。

今回のサンプルのページには「Runtime Requirements: iOS 6.0 or later.」となっていましたが、iOS5でもネットワークの状態の変化を検知できました。

さて、手元にReachabilityのバージョン2.2があるので、新しいバージョン3.0と見比べてみます。ヘッダファイルのクラスメソッドの返り値が全部instancetypeになっていますね。最近自分もよくやってます。

あとは変数名retValがreturnValueになっていたり、スペースの使い方が変わってたり、コメントが//〜から/*〜*/に変わってたりどうでもいい変更ばっかりでした。ARC対応もautoreleaseとAutoreleasePoolを消したくらい。

*1:ちなみにサンプルコード自体はログインしなくても見れるので、NDA範囲外でしょう

appleapple2014/01/21 16:1571をコピペまるまるしたんですが#import "VoiceSwitch.h"ここの部分でファイルノットファウンドがでるんですが、どうすればよろしいですか?

ktakayamaktakayama2014/01/23 16:56すいません、Reachabilityのことではないですよね…。
ちょっとよくわからないのですが、VoiceSwitch.hというファイルがプロジェクトに入ってないのではないでしょうか?

トラックバック - http://iphone-dev.g.hatena.ne.jp/ktakayama/20130822

2013-08-14 (Wed)

MountainLionで発生するEventKitの不具合についてレポートを送る 17:32  MountainLionで発生するEventKitの不具合についてレポートを送る - iOS プログラミングメモ を含むブックマーク

みなさん、バグっぽいもの見つけたらバグレポートしましょうねということで。バグレポは以下から送れます。たぶん英語で送らないといけないのでなかなか躊躇しちゃいますが、躊躇しててもバグは直らないので積極的に送りましょう。自分の場合は箇条書き+サンプルプロジェクトを付けて送っていて、なんとかなってる感じです。直ったって連絡くると、嬉しいですよ。

特にいまはバグチェックが目的の一つでもあるベータ版のOSが出てますから、一つくらいはバグを見つけて貢献したいですね。

そんで今回報告するバグですが、EventKit.frameworkで二つあります。MoutainLionからiOSと同じインターフェイスのEventKit.frameworkが使えるようになって、カレンダーアプリ開発者にとっては幸せいっぱいですが、少しバグがあるんですね。EventKitを使う人がどれだけ居るかわかんないけど、参考までに。

デイリーの繰り返しイベントを編集すると、データがおかしくなる

  • 事前準備としてEKRecurrenceFrequencyDailyで繰り返しイベントを登録する → (A)
  • (A)のイベントの中から、最初の日以外のイベントの内容を変更して"saveEvent:event span:EKSpanFutureEvents"みたいに「今後のイベントも一緒に変更」ってやると、選択したイベントの前日のイベントが消える。
  • 同様に、(A)のイベントの最初の日のイベントに対してEKSpanFutureEventsとして変更・保存すると、イベントが1個増える。

EventKit触ったことあってもわかりにくい文章だな…。まあとにかくこんな感じ変な動きをする。EKRecurrenceFrequencyWeeklyとかMonthlyとかだと大丈夫という。

Eventを作成してロードすると、登録してないAlarmが登録されている

※標準のカレンダーアプリの環境設定で「通知」を設定すると、デフォルトでAlarmが登録されるようになるけど、それとはまた別問題です。

  • このAlarmのabsoluteDateは1976年04月01日とかになっている。
  • このAlarmは消しても消えない。
  • EventKitの前身であるCalCalendarで同じイベントをロードすると、CalAlarm.actionがNoneとして登録されているのが確認できる。
    • ちなみにCalAlarmのactionに指定できる定数には「None」なんて存在しない。
extern NSString * const CalAlarmActionDisplay;
extern NSString * const CalAlarmActionEmail;
extern NSString * const CalAlarmActionProcedure;
extern NSString * const CalAlarmActionSound;
トラックバック - http://iphone-dev.g.hatena.ne.jp/ktakayama/20130814

2013-08-12 (Mon)

NSScrollViewをアニメーションしながらスクロールさせる 17:18 NSScrollViewをアニメーションしながらスクロールさせる - iOS プログラミングメモ を含むブックマーク

こんな何でも無いことが、意外とめんどくさいのよね。

特定の位置までスクロールさせるだけなら、以下のような感じ。アニメーションはしない。

NSScrollView *scrollView = ...;
[scrollView.documentView scrollPoint:NSMakePoint(x, y)];

NSViewはNSAnimatablePropertyContainerていうなかなか便利なプロトコルが使えるようになっていて、animatorていうプロキシを通すと勝手にアニメーションしてくれる。たとえば以下のようにframeの値を変えると、アニメーションしながらリサイズしてくれる。

NSView *someView = .../
[[someView animator] setFrame:NSMakeRect(x, y, width, height)];

という感じでscrollPointもanimatorを通せばアニメーションしてくれる…と思いきや、これは全然うまく動かない。アニメーションなしで、ぱっと切り替わる。

[[scrollView.documentView animator] scrollPoint:NSMakePoint(x, y)];

どうもよくわかんないんだけど、自前でアニメーション用のコードを書かないといけないっぽい雰囲気。Duxっていうエディタのコードに、まさにそのものが含まれているらしい。

というわけで、最終的にこんな感じに。

[DuxScrollViewAnimation animatedScrollToPoint:NSMakePoint(x, y) inScrollView:scrollView];

しかしまだなんか位置が微妙にずれてる気がしないでもないが…。アニメーションはするようになった。

ちなみのDuxは Unlicense なる結構自由なライセンスのようです。ステキ。

トラックバック - http://iphone-dev.g.hatena.ne.jp/ktakayama/20130812

2013-08-09 (Fri)

[] NSStringの文字列比較はisEqualToStringではなくてcompareメソッドがいいのか 16:52  NSStringの文字列比較はisEqualToStringではなくてcompareメソッドがいいのか - iOS プログラミングメモ を含むブックマーク

Appleのサンプルコード DownloadFont を眺めてたら、NSStringの文字列比較にcompare:メソッドを使っていて「あれisEqualToStringじゃないの?」って思ったので調べてみたら意外と重大な事実が判明しました。isEqualToStringはNSLiteralSearchオプションによる比較なので、見かけ上同じ文字列だったとしてもバイト列が異なる場合はNOを返すらしい。うはー、こんな基本的なことを知らなかったとか恥ずかしいです。

たとえば以下のコードの場合ですとisEqualToStringは通りませんが、compare:はNSOrderedSameを返すので真になります。

NSString *stringA = @"\343\203\221\343\202\272\343\203\211\343\203\251";
NSString *stringB = @"\343\203\217\343\202\232\343\202\271\343\202\231\343\203\210\343\202\231\343\203\251";

if([stringA isEqualToString:stringB]) NSLog(@"isEqualToString");
if([stringA compare:stringB] == NSOrderedSame) NSLog(@"compare");

stringAが「パズドラ」という文字で、stringBは濁点と半濁点が分離した文字になっています。Macのファイルシステムで使われてる UTF-8-MAC がこの分離文字を扱ってるので、結構おなじみかもしれません。

そういえば拙作の SkyBook でも、内蔵ストレージに保存した濁点付きの文字列の比較がうまくいかなくて困った経験がありました。その時は分離した文字列をくっつけるように変換して対処しましたが、compare:使うのが正解だったのか!!

参考までにSkyBookでやったやり方はこちら。

NSString *stringA = @"\343\203\221\343\202\272\343\203\211\343\203\251";
NSString *stringB = @"\343\203\217\343\202\232\343\202\271\343\202\231\343\203\210\343\202\231\343\203\251";

NSMutableString *stringC = [NSMutableString stringWithString:stringB];
CFStringNormalize((CFMutableStringRef)stringC, kCFStringNormalizationFormC);

if([stringA isEqualToString:stringC]) NSLog(@"great!!");

これだとgreat!!と出力されます。