人生ずっと勉強

人生ずっと勉強ですね。 https://twitter.com/KiyotakaGoto

boundingRectWithSize:options:attributes:context: で計算した値を元に View の size を設定すると文字が切れる問題

問題

boundingRectWithSize:options:attributes:context で計算した値を
そのまま UILabe やら UITextView の size の値に設定すると、文字が切れてしまう。

解決法

ドキュメントに書いてあるまんまなのですが、

This method returns fractional sizes (in the size component of the returned CGRect); to use a returned size to size views, you must use raise its value to the nearest higher integer using the ceil function.

ということで、ceil() してあげましょう、とのこと。

レイアウトの階層を表示

Android だとレイアウトの階層構造が見れる hierarchy view perspective があるんですが、
Xcode だとそういうのがパッと見みつからなかったのでどうやってレイアウトの状態を把握するのだろうと思っていたところ、教えてもらったのでメモ。

UIView に対して recursiveDescription を呼び出す

通常のメソッド呼び出しでは呼べないので performSelector する。
UIViewController の view の階層を見るときは以下の様な感じ。

NSLog(@"%@", [self.view performSelector:@selector(recursiveDescription)]);

出力は以下の様な感じ。

2013-08-17 21:04:53.358 UIViewControllerTest[60940:11303] <UIView: 0x76935f0; frame = (0 20; 320 460); autoresize = W+H; layer = <CALayer: 0x7693650>>
   | <UIRoundedRectButton: 0x7692130; frame = (25 397; 63 44); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x7691940>>
   |    | <UIGroupTableViewCellBackground: 0x7692200; frame = (0 0; 63 44); userInteractionEnabled = NO; layer = <CALayer: 0x7691970>>
   |    | <UIImageView: 0x76929d0; frame = (1 1; 61 43); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7692380>> - (null)
   |    | <UIButtonLabel: 0x7692480; frame = (12 12; 39 19); text = 'show'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7692520>>
   | <UIView: 0x76932f0; frame = (0 0; 320 224); clipsToBounds = YES; autoresize = LM+RM; layer = <CALayer: 0x7693290>>
   | <UIRoundedRectButton: 0x768df00; frame = (232 412; 55 44); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x768e020>>
   |    | <UIGroupTableViewCellBackground: 0x768e800; frame = (0 0; 55 44); userInteractionEnabled = NO; layer = <CALayer: 0x768e8d0>>
   |    | <UIImageView: 0x768f940; frame = (1 1; 53 43); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7690050>> - (null)
   |    | <UIButtonLabel: 0x768f520; frame = (12 12; 31 19); text = 'hide'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x768f610>>
   | <UIRoundedRectButton: 0x7692710; frame = (229 361; 71 44); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x76927e0>>
   |    | <UIGroupTableViewCellBackground: 0x7692810; frame = (0 0; 71 44); userInteractionEnabled = NO; layer = <CALayer: 0x7692890>>
   |    | <UIImageView: 0x7693770; frame = (1 1; 69 43); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7693e90>> - (null)
   |    | <UIButtonLabel: 0x7693fa0; frame = (12 12; 47 19); text = 'switch'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7694040>>
   | <UIRoundedRectButton: 0x7691190; frame = (96 412; 129 44); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x7691260>>
   |    | <UIGroupTableViewCellBackground: 0x7691290; frame = (0 0; 129 44); userInteractionEnabled = NO; layer = <CALayer: 0x7691310>>
   |    | <UIImageView: 0x7691380; frame = (1 1; 127 43); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x76913e0>> - (null)
   |    | <UIButtonLabel: 0x7691540; frame = (12 12; 104 19); text = 'view hierarchy'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x7691630>>

ログ出力なのであまり見やすくはないけど、何もないよりはマシですね。

View のない non-UI ( non-graphical ) fragment の1つの使い道

fragment に関する公式ドキュメントを読んでると、UI を持たない fragment を作ることもできるとあるけれど、一体何に使うのかよくわかりませんでした。
ところが最近、会社の先輩からその1つの使い道を教えてもらったので、忘れないようにメモ。

DialogFragment の Helper クラスとして使う

たとえば、以下の様な要件があるときに、non-UI fragment が使えます。

  • ネット上の画像を取得して、DialogFragment の layout にその画像を表示させたい
  • Activity に LoaderCallbacks を implements させたくない

このとき、以下のように素直に DialogFragment を継承したクラスに LoaderCallbacks を implements させて AsyncTaskLoader を呼びだそうとすると、
「IllegalStateException: Fragment not attached to Activity」が発生します。

public class SampleDialogFragment extends DialogFragment implements LoaderCallbacks<Bitmap>{
    private FragmentManager mManager;
    private String mTag;
    private Bitmap mImageData;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View dialogView = inflater.inflate(R.layout.dialog_fragment_layout, container, false);

        ImageView imageView = (ImageView) dialogView.findViewById( R.id.image );
        imageView.setImageBitmap( mImageData );
        return dialogView;
    }

    // Activity からはこのメソッドを呼び出す
    public void showDialog ( FragmentManager manager, String tag ) {
        mManager = manager;
        mTag = tag;
        getLoaderManager().initLoader(0, null, this );
    }

    @Override
    public Loader<Bitmap> onCreateLoader(int arg0, Bundle arg1) {
        return new SomeAsyncTaskLoader( getActivity() );
    }

    @Override
    public void onLoadFinished(Loader<Bitmap> arg0, Bitmap imageData) {
        mImageData = imageData;
        show( mManager, mTag );
    }

    @Override
    public void onLoaderReset(Loader<Bitmap> arg0) {}
}

LoaderManager は Activity/Fragment のライフサイクルと連動して動くみたいなんですが、
上記の showDialog () を呼んだ時点ではまだこの fragment 自体が activity に紐付いておらず、activity のライフサイクルと連動していないので、結果としてLoaderManager が連動するライフサイクルが定まらないためにこの Exception が投げられるのかなと思います。

これを回避するために、UI を持たないフラグメントをただのインタフェースとして用意し、Activity にそのフラグメントを追加し、Activity からはそのフラグメントが提供するメソッドを介して DialogFragment を表示させるようにします。
おおまかな手順は、

  • non-UI fragment を用意し、LoaderCallbacks をこれに implements する。
  • Activity で non-UI fragment を add する
  • Activity から non-UI fragement が提供するメソッドが呼び出されたら、そのメソッド内で loader を起動する
  • onLoadFinished() で 実際の DialogFragment を呼び出す

という感じです。

しかし、まだ問題があります。

Can not perform this action inside of onLoadFinished

たとえば素直に onLoadFinished() 内で DialogFramgnet のインスタンスを生成し、表示させようとします。

    @Override
    public void onLoadFinished(Loader<Bitmap> arg0, Bitmap imageData) {
        SampleDialogFragment dialog = new SampleDialogFragment();
        dialog.show( getFragmentManager(), TAG, imageData );
    }

すると以下の様な exception が投げられます。

FATAL EXCEPTION: main
java.lang.IllegalStateException: Can not perform this action inside of onLoadFinished

これについては、以下の stack over flow あたりを参考に回避します。
android - This Handler class should be static or leaks might occur: IncomingHandler - Stack Overflow

もろもろまとめると

サンプルプロジェクトを以下に置いてみました。
kiyotakagoto/NonUIFragmentSample · GitHub

Android で View の Id が取得できない場合にコンテンツ表示領域のサイズを取る方法

コンテンツ表示領域の width, height を取得したい場合、通常なら
Activity#findViewById() して取得した ViewGroup の getter である getWidth, getHeight を使えばいい。

一方、たとえば SDK の開発などで、
SDK組み込み先のアプリのコンテンツ表示領域の特定位置に何らかの View (fragmentなど)を表示させたい”
といったときは、view の id がわからず、この方法が使えないこともある。
そういうときは、以下のように、 root のレイアウトを取得する。

ViewGroup root = (ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content);
root.getHeight();
root.getWidth();

znc であとから webadmin を使いたくなった時

  • webadmin - ZNC にある通り、何らかの IRC クライアントで以下のコマンドを発言をする。
/znc LoadMod webadmin

ポートは、~/.znc/configs/znc.conf の タグに設定してある番号と同じ。

  • ブラウザでアクセス(一部のポート番号は chrome だと見れないことがあるらしい)

Bing API on Ruby

気をつけること

  • Bing Search API – Quick Start and Code Samples に載ってるサンプル php コードで指定している Authorization ヘッダの指定の仕方は間違っているので真似しない。実際はアカウント名は空文字列、パスワードに accout_key を使う。
  • なぜかクエリはシングルクォーテーションで囲まないと認識されない
  • net/http, net/https のどちらでも叩ける
  • HTTP メソッドは POST でも GET でもどちらでもよいっぽい

Sample code

net/http, Net::HTTP::Get ver.

require 'net/https'

class BingWrapper

  def initialize( account_key )
    @account_key = account_key
    @uri_base    = 'https://api.datamarket.azure.com/Bing/Search/Image?$format=json&Query='
  end

  def get( unencoded_query )
    # I don't know why, but single quotaition is needed to specify a Query.
    uri = URI.parse( "%s'%s'" % [@uri_base, URI.encode( unencoded_query )] )

    req = Net::HTTP::Get.new( uri.request_uri )
    req.basic_auth( '', @account_key)
    res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https'){ |http|
      http.request(req)
    }

    return res.body
  end
end

net/https, Net::HTTP::Post ver.

require 'net/https'

class BingWrapper

  def initialize( account_key )
    @account_key = account_key
    @uri_base    = 'https://api.datamarket.azure.com/Bing/Search/Image?$format=json&Query='
  end

  def get( unencoded_query )
    # I don't know why, but single quotaition is needed to specify a Query.
    uri = URI.parse( "%s'%s'" % [@uri_base, URI.encode( unencoded_query )] )

    https = Net::HTTP.new( uri.host, uri.port )
    https.use_ssl = true
    https.verify_mode = OpenSSL::SSL::VERIFY_NONE

    req = Net::HTTP::Post.new( uri.request_uri )
    req.basic_auth( '', @account_key )
    res = https.request( req )
    return res.body
  end
end

余談

    res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https'){ |http|
      http.request(req)
    }

この書き方いいなぁと思った。

レシーバが nil だった場合の戻り値

ハマったのでメモ。
詳解 Objective-C 2.0 第3版 の63ページより。

返り値の型 返り値
オブジェクト nil
ポインタ NULL
整数 0
それ以外 OS のバージョンなどで異なるため、不定と考えるのが無難

背景

UILocalNotification に設定した通知を削除するために、設定時に userInfo に適当な値を設定して、
キーをもとに通知を消す以下の様なコードを書いた。

- (void) setNotification {
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    
    notification.fireDate = [[NSDate date] addTimeInterval:60]; 
    notification.timeZone = [NSTimeZone defaultTimeZone];
    notification.alertBody = @"Notification";
    notification.alertAction = @"Open";
    notification.soundName = UILocalNotificationDefaultSoundName;
    notification.applicationIconBadgeNumber = 1;
    [ notification.userInfo setValue:@"set" forKey:@"notificationTime" ];

    [[UIApplication sharedApplication] scheduleLocalNotification:notification];
}

- (void) deleteNotification {

    for(UILocalNotification *notification in [[UIApplication sharedApplication] scheduledLocalNotifications]) {

        if(  [notification.userInfo objectForKey:@"notificationTime"]  ) {
            [[UIApplication sharedApplication] cancelLocalNotification:notification];
        }
    }

}

しかし、エラーも何も起こらず、 if 内に分岐しない。
デバッグしてみると、そもそも notification.userInfo プロパティは NSDictionary 型で、何らかのNSDictionary 型のインスタンスを生成して値として設定しない限り nil であることがわかった。

同時に、そういえばレシーバが nil のときの挙動について何かで見たぞと思いだし、詳解 objective-c を調べてみると、やはりそうだった、というオチ。
なので、以下のように修正。

notification.userInfo = @{ @"notificationTime":@"set" };