甘いものが好きです

iOS App開発時に感じた疑問や課題、その他の雑感などを書いていきます。

文字列(NSStringオブジェクト)を画像化する方法

文字列を画像化する方法を調べてみたら、オフスクリーン描画をしてグラフィックスコンテキストから画像を得る方法が簡単だった。

上記のページを参考にして、指定されたサイズの領域の中央に文字列を描画して画像化するコードを書いてみた。

- (UIImage *)imageWithText:(NSString *)text fontSize:(CGFloat)fontSize rectSize:(CGSize)rectSize {

    // 描画する文字列のフォントを設定。
    UIFont *font = [UIFont systemFontOfSize:fontSize];

    // オフスクリーン描画のためのグラフィックスコンテキストを作る。
    if (UIGraphicsBeginImageContextWithOptions != NULL)
        UIGraphicsBeginImageContextWithOptions(rectSize, NO, 0.0f);
    else
        UIGraphicsBeginImageContext(rectSize);

    /* Shadowを付ける場合は追加でこの部分の処理を行う。
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetShadowWithColor(ctx, CGSizeMake(1.0f, 1.0f), 5.0f, [[UIColor grayColor] CGColor]);
    */

    // 文字列の描画領域のサイズをあらかじめ算出しておく。
    CGSize textAreaSize = [text sizeWithFont:font constrainedToSize:rectSize];

    // 描画対象領域の中央に文字列を描画する。
    [text drawInRect:CGRectMake((rectSize.width - textAreaSize.width) * 0.5f,
                                (rectSize.height - textAreaSize.height) * 0.5f,
                                textAreaSize.width,
                                textAreaSize.height) 
            withFont:font 
       lineBreakMode:NSLineBreakByWordWrapping 
           alignment:NSTextAlignmentCenter];

    // コンテキストから画像オブジェクトを作成する。
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return image;
}

NSStringクラスのインスタンスメソッドsizeWithFont:constrainedToSize:は文字列の描画領域のサイズをあらかじめ算出しておくのに便利だ。文字列の長さやフォントに応じてラベルのサイズなどの調整を行いたいときにも使える。

ビルドエラー「While reading ... pngcrush caught libpng error」への対処方法

Xcodeプロジェクトで使用している画像を新しいものに差し替えた後、実機をターゲットにしてビルドすると以下の3つのエラーが出るようになった。*1

  • While reading [プロジェクトで使用している画像へのフルパス*2] pngcrush caught libpng error:
  • Could not find file: [ビルド後に作成されるapp内の画像ファイルへのフルパス*3]
  • Command /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/copypng emitted errors but did not return a nonzero exit code to indicate failure

ビルドエラーとなるもののデバック実行は正常に開始された。これは第3のエラーの「emitted errors but did not return a nonzero exit code to indicate failure」という部分によるものかもしれない。

原因と対処方法

エラー内容で検索してみると、以下のページを発見した。

どうやら差し替えた後の画像が「インターレースあり」になっていたことが原因のようだ。実際、問題の画像を「インターレースなし」のものに変更してからはこれらのビルドエラーが出なくなった。

そもそもインターレースとは?

「そもそもインターレースとは?」という話については、次のページなどが参考になる。

*1:確認時の環境は次のとおり。OS X 10.8.2、Xcode 4.5.2 (4G2008a)、Base SDK: iOS 6.0、Deployment Target: iOS 5.0。

*2:例: /Users/Someone/Documents/Source/MyProject/Default.png

*3:例: /Users/Someone/Library/Developer/Xcode/DerivedData/MyProject-dtarawgdyiebqidxebcbowwltlxp/Build/Products/Debug-iphoneos/MyProject.app/Default.png

In-App PurchaseでProductsRequestの応答にInvalid Product IDが含まれる場合にチェックすべきこと

In-App Purchase(アプリ内課金)で課金対象プロダクトについての情報を取得するときなどには、SKProductsRequestオブジェクトに対してstartメッセージを送信する。

- (void)sendProductsRequestWithProductIdentifiers:(NSSet *)identifierSet delegate:(id)productsRequestDelegate {
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:identifierSet];
    request.delegate = productsRequestDelegate;
    [request start];
}

この要求への応答はdelegateに対するproductsRequest:didReceiveResponse:で得られる。

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    // [response.invalidProductIdentifiers count]は0であることが期待される。
}

通常は応答に含まれるinvalidProductIdentifiersの要素数は0であることが期待される。ここに無効なプロダクトのIDが含まれている場合には、何らかの問題が発生している可能性がある。下記ページに掲載されていたチェックリストが大変参考になった。

以下でそのチェックリストを引用し、項目の詳細についていくつか注記する。

  • Have you enabled In-App Purchases for your App ID?*1
  • Have you checked Cleared for Sale for your product?*2
  • Have you submitted (and optionally rejected) your application binary?
  • Does your project's .plist Bundle ID match your App ID?*3
  • Have you generated and installed a new provisioning profile for the new App ID?
  • Have you configured your project to code sign using this new provisioning profile?
  • Are you building for iPhone OS 3.0 or above?
  • Are you using the full product ID when when making an SKProductRequest?*4
  • Have you waited several hours since adding your product to iTunes Connect?*5
  • Are your bank details active on iTunes Connect?*6
  • Have you tried deleting the app from your device and reinstalling?
  • Is your device jailbroken? If so, you need to revert the jailbreak for IAP to work.

*1:使用しているApp IDについて、In-App Purchaseが有効に設定されているか。iOS Provisioning Portalで確認できる。

*2:プロダクトが「Cleared for Sale」になっているか。iTunes ConnectでIn-App Purchaseの課金対象の各プロダクトに関する設定項目を表示するページで確認できる。

*3:Xcodeプロジェクトのinfo.plistに設定されているBundle IDとして、iOS Provisioning Portalで作成した正しいApp IDが設定されているか。

*4:SKProductRequestに設定するプロダクトIDが正しいか。実際にSKProductsRequestオブジェクトの初期化で使用しているプロダクトIDと、iTunes Connectでプロダクトごとに設定しているIDが完全に一致しているか。

*5:iTunes Connectに新規プロダクトを追加してからそれが反映されるまでには時間がかかる。

*6:iTunes Connectの「Contracts, Tax, and Banking」での設定が不十分である場合には課金対象プロダクトが無効扱いになる。

警告「PerformSelector may cause a leak because its selector is unknown」への対処

セレクタを指定してオブジェクトにメッセージを送る処理について。

SEL sel = @selector(doSomething);
if ([obj respondsToSelector:sel]) {
    [obj performSelector:sel];
}

このようなコードをビルドすると、ARC有効時にperformSelector:の行について次の警告が出る。

PerformSelector may cause a leak because its selector is unknown

指示子@selectorによりセレクタを指定するのではなくSEL型変数を使用する場合にのみ警告が出る。実際、次のような場合には警告は出ない。

if ([obj respondsToSelector:@selector(doSomething)]) {
    [obj doSomething];
}
if ([obj respondsToSelector:@selector(doSomething)]) {
    [obj performSelector:@selector(doSomething)];
}

原因

この警告が出る原因に関するAppleへの問い合わせ結果が上記記事に記載されていたのを発見。参考になりました。ありがとうございます。

 いきなり、-performSelector:でワーニング出やがるし〜

 結局、2つあるインシデントの1つを使ってAppleに問い合わせたら「-performSelector:からの返り値をコントロールできなくてワーニング出るんだよ。

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:ac];
#pragma clang diagnostic pop

 で無視するようにしちゃってー。」て返答が返ってきますた。まあ、結局元々のToll-free bridge用の対応でいけるって事みたいです。
 正式回答なんで、皆さん、安心して上記対応でいきましょう。

対処方法

以下のページを参考にしつつ対処方法を整理する。*1

方法1: 警告を局所的に無視する

次のように警告の対象となる箇所を挟み込む形で#pragma指令を書くことにより、警告の種類を指定して局所的に警告を無視するように設定することができる。*2

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [obj performSelector:sel];
#pragma clang diagnostic pop
方法2: ランタイム関数を使用する

performSelector:ではなくランタイム関数objc_msgSend()によりメッセージを送信すると警告が出ない。

#import <objc/message.h> // objc_msgSend()を使用するために必要

// 中略

if ([obj respondsToSelector:sel]) {
    objc_msgSend(obj, sel);
}

方法3: performSelector:withObject:afterDelay:を使用する

performSelector:のかわりにperformSelector:withObject:afterDelay:を使うと警告が出ない。performSelector:withObject:afterDelay:が値を返さないことと、「performSelector:からの返り値をコントロールできなくてワーニング出る」というAppleからの回答内容を考えると、この結果は当然のものといえるだろう。

if ([obj respondsToSelector:sel]) {
    [obj performSelector:sel withObject:nil afterDelay:0.0f];
}

*1:確認時の環境は次のとおり。OS X 10.8.2、Xcode 4.5.2 (4G2008a)、Base SDK: iOS 6.0、Deployment Target: iOS 5.0、ARC: 有効。

*2:Apple LLVM compiler 4.1で確認。

警告「Unsupported Configuration: … view outlet and NIB Name set」への対処

XcodeでiOS Appのプロジェクトをビルドしたときに、xibファイル内のView Controllerについて次のような警告が出ることがあった。*1 *2

Unsupported Configuration
… view outlet and NIB Name set

「view outlet and NIB Name set」で検索してみると、この警告に遭遇した人が少なくないことがわかる。

上記ページを参考に、自分がビルドしているプロジェクトで問題となっているxibファイルの内容を確認してみたところ、次の状況において、第2項と第3項を同時に設定しているときにこの警告が出ることが判明した。

  1. File's OwnerがView Controllerで、他View Controllerのオブジェクト(ここでは「外部View Controller」と呼ぶ)を追加している。
  2. 外部View Controllerのviewのオブジェクトもxibファイル内に追加している。
  3. 前項の外部View Controllerオブジェクトについて、Identity Inspectorで「NIB Name」を指定している。

今回は、外部View ControllerオブジェクトをそのView Controllerのクラスに対応する別のxibファイルから作成するように意図していたため、外部View Controllerのviewオブジェクトをxibファイルから削除した。この修正により、警告は出なくなった。

*1:確認時の環境は次のとおり。OS X 10.8.2、Xcode 4.5.2 (4G2008a)、Base SDK: iOS 6.0、Deployment Target: iOS 5.0。

*2:「…」の部分には、View Controllerオブジェクトのクラス名から自動生成された文字列が入る。

有効ではないSEL型変数にはnilとNULLのどちらを設定すべきか

有効ではないSEL型変数にはnilとNULLのどちらを設定すべきか、少々気になったので調べてみた*1

NULLにするのが「伝統的」

『詳解 Objective-C 2.0 第3版』の08-02節「メッセージ送信の仕組み」(185ページ)には次のように書かれている。

Objective-Cプログラマは、SEL型が具体的にどんな値を持つのかについて知っている必要はありません。詳細は処理系に依存します。ただし、SEL型の変数が有効なセレクタを値として持っていないことを表したい場合は、伝統的にNULLを使うことが多いようです。あるいは、(SEL)0という表現でもよいでしょう。

詳解 Objective-C 2.0 第3版

詳解 Objective-C 2.0 第3版

なるほど。有効ではないSEL型変数にはNULLか(SEL)0を設定するのが適切であるようだ。

SEL型の実体はC言語タイプの文字列

SEL型の詳細については『Dynamic Objective-C』が次の場所で詳しく解説している。

  • Section 18「メソッドとは何か(1)—メソッド、セレクタ、メソッドの実装」(64ページ)
  • Section 21「メソッドとは何か(4)—セレクタの実体」(72ページ)

これらの部分で、SEL型が実はNULL文字で終わるC言語タイプの文字列であることが説明されている*2。このことからも、有効ではないSEL型変数に設定する値としてはNULLが適切であるといえるだろう。

Dynamic Objective-C

Dynamic Objective-C

『Dynamic Objective-C』は、単にObjective-Cの使い方を説明しているのではなく、Objective-Cという言語がどのようなつくりになっているのかというレベルで解説している。そのため、普段Objective-Cのコードを書く上で目にしない部分を扱うことも多く、読み解くのに少々苦労することもあるが、今回のような疑問を解く上では非常に参考になる1冊だ。手元に置いてはあるのだがまだきちんと読めていないので、そろそろじっくりと読み解いてみたい。

*1:nilとNULLはそれぞれ以下のように定義されているので、実質的には同じような気もするのだが……。
#define NULL ((void*)0)
#define nil NULL

*2:今回初めて知った。自分はまだまだObjective-Cの奥深さを知らずにいるのだと改めて自覚した。

Mac App Storeで「ほかのアカウントで使用可能なアップデートがあります」と出てアップデートできない場合の対処法(追記あり)

Mac App Storeでアップデートを試みるとエラーが発生する

OS X Mountain Lionのリリースに合わせてXcode v4.4もMac App Storeで公開された。さっそくアップデートをしようと思い、Mac App Storeの「購入済み」タブからXcodeのアップデートを試みたところ、次のようなエラーが出てアップデートができなかった。

ほかのアカウントで使用可能なアップデートがあります
このアプリケーションをアップデートするには、購入時に使用したアカウントでサインインしてください。

Spotlightのインデックス再構築をするとアップデート可能に

このエラーメッセージで検索をしたところ、「Mac App Storeでのアップデートエラー」というタイトルのページで、Spotlightのインデックスを再構築をすることによりアップデートが可能になるという情報を得た。

私は Spotlight を使用しない設定にしていたので、これを元に戻して / が検索対象となるようにインデックスの再構築を行いました。
# 58時間とか出てちょっとげんなりしましたが
↓ここにその方法でうまくいった人がいます。
http://discussions.apple.com/thread.jspa?messageID=13006522&tstart=0

Spotlightのインデックスの再構築は、ターミナルでコマンドを打つ方法と、システム環境設定のウィンドウで操作する方法の2つがある。次のページでそれぞれの方法が解説されている。

Spotlightのインデックス再構築が終わり次第、再びMac App StoreXcodeのアップデートを試してみると、今度は無事にアップデートすることができた。*1

アップデート後にもう一度Spotlightのインデックスを再構築しないとダメ?

余談だが、アップデート完了後にMac App Storeの「購入済み」タブを再び表示してみると、Xcodeの購入日に変化がなく、「アップデート」ボタンが表示されたままになっていた。そこで再びSpotlightのインデックスを再構築してみたところ、ようやく「アップデート」ボタンが表示されていたところが「インストール済み」の表示に変化した。

Spotlightのインデックスを再構築しない方法(2012/11/03追記)

後日 @EasyStyleGKさんから別の方法を教えていただいたので、その方法について追記しておく。

対象のアプリケーションをアプリケーションフォルダから削除することでもアップデート可能になるとのこと。私自身はこの方法を試したことはないが、Spotlightのインデックス再構築が終わるまで長時間待つことができない状況では、この方法を試してみるのもよいかもしれない。


*1:Xcodeのメニューから「About Xcode」を選択し、「Version 4.4 (4F250)」と表示されることを確認。