2013年7月15日月曜日

あまりにも暑いので、やってみた。




なお、QRコードはcmanで生成しました。文字列はwikipediaから。

QRコード落語、とか思いついたけど、「子別れ」ってタイトルの後に何百個並べればいいのかわからないのでやめた。

2013年7月10日水曜日

サンダーバードの未来との差分

香港にて。

朝、サンダーバードの展示会についてのニュースをやっていた。

その中で、あれもこれも実用化されたけど、当時は携帯電話もインターネットも想定外だったねぇ、みたいな話が出ていた。

確かに、電子機器の発達は異常なほどだけど、交通機関は意外と発展してない気がする。チューブ式の高速鉄道や、車輪のない空飛ぶ自動車はどうなったんだw もちろん、サンダーバードの頃に比べれば、空の旅は特別なものではなくなっているし発展途上国でも自動車が多すぎて公害問題を引き起こしてたりと、量的な問題は大きく変わっているんだけども。

でも、ニュースを見ながら思った。電話など通信技術の発達が移動手段の進歩を止めちゃったんじゃないかなー。話をするなら顔を合わせなくちゃいけないし、必死こいて高速移動な未来を目指していたんだけど…どこにいても海外とも無料で顔を見ながら話ができる世の中になっちゃったら、余計な放射線浴びながら高々度超高速ジェット機乗ろうって気分も無くなっちゃう…ような気がする。

ついでにいえば、公共交通機関がきちんと定時運行されることは高速化と同じぐらいの意味があるよね。時間の読めない乗り物だとそれなりにマージンを見ないといけないけど、マージンの30分も電車に乗ってる30分も同じだから、みんながそのマージンを織り込まなくて済むということは、電車が30分速くなったのと同じこと。

なのでJRさん、はやくホームドアを全駅に設置してくださいね。

iPhoneのキーボードが出た時にUITextFieldの高さを変える方法(iOS 6)

よく、ブログなどに書かれているのは、「出てきたキーボードの高さ分をUITextFieldのheightから引いてやる」って感じだけど…それだと英語キーボードから日本語キーボードに切り替え、さらに予測変換窓が出た時に過剰に小さくなってしまったりします。

ということで、それを回避できるコードを張っておきます。UITextFieldが画面一杯に貼ってあるのが前提です。

■準備:Notificationの設定■

UIKeyboardWillShowNotificationでkeyboardWillShowを呼び出すよう設定します。


- (void)viewDidLoad
{
    [super viewDidLoad];

    NSNotificationCenter *center;
    center = [NSNotificationCenter defaultCenter];
    [center addObserver:self
               selector:@selector(keyboardWillShow:)
                   name:UIKeyboardWillShowNotification
                 object:nil];
}


■本体■

UIKeyboardWillChangeFrameっていうnotificationが別にあるんですが、willShowだけでキーボードが現れる時、英語と日本語切り替えた時、日本語キーボードで予測変換窓が現れた時に呼び出されてしまいます。良いのかそれで?

ミソは、convertRectです。keyboardFrameEndから返ってくるのはwindowの座標系で、ツールバーの高さやOrientationなどが配慮されていません。ので、現在のviewの座標系に変換します。これで「キーボードのorigin.y座標=使える画面の下限座標」となりますから、これでtext fieldの高さを設定し直します。

同じViewの上でキーボードを出したり消したりする必要のある時にはUIKeyboardWillHideNotificationを使います。ノリは同じで、viewのframeを取得してその下限を元にtext fieldなどの高さを調整するだけです。



- (void)keyboardWillShow:(NSNotification*)notification
{
    CGRect keyboardFrameEnd = [[notification.userInfo 
           objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    keyboardFrameEnd = [self.view convertRect:keyboardFrameEnd
                                     fromView:self.view.window];
    
    originFrame = self.textInput.frame;    
    float newHeight = keyboardFrameEnd.origin.y - originFrame.origin.y;

    [self.textInput setFrame:CGRectMake(originFrame.origin.x, 
                                        originFrame.origin.y, 
                                        originFrame.size.width, 
                                        newHeight)];
}


■後始末■

最後にnotificationを消し忘れないように。私はよく忘れます。ではでは。


- (void)dealloc {
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self 
                      name:UIKeyboardWillShowNotification 
                    object:nil];

    [textInput release];
    [super dealloc];
}

2013年7月9日火曜日

古いアプリをiOS6対応にしたらupside downにならない件を解決

■症状■

古いプロジェクトをiOS 6対応にしたら、portrait upside downにならない。NotificationでOrientationが変わったことは検出できるけど、Viewが回転しないので横向きになったまま。

iOS6のデフォルトではiPhoneではupside down以外が許可、iPadでは全方向が許可されていますので、それがそのまま効いていて変更できていない、ということになります。


■一般的な対策■

iOS 6ではshouldAutorotateToInterfaceOrientationが廃止になったので、対象ViewControllerでsupportedInterfaceOrientationsとshouldAutorotateを使う。


- (BOOL)shouldAutorotate {
    return YES;
}

-(NSUInteger)supportedInterfaceOrientations{
    return UIInterfaceOrientationMaskAll;
}

iOS 6対応として最初から作ったプロジェクトはこれで動きます。しかし、viewやらcontrollerをいろいろいじっている古くからのプロジェクトはこれではダメでした。

個人的困ったのはSalesforce対応アプリです。Salesforce mobile SDK for iOSの最新版ver 1.5でも未だにStoryboardやARCに対応していないので、中身は結構古いまま。テンプレートからiOS 6 targetにしても、upside downは働いてくれません。

その一方で…iOS 5をターゲットにすると、Web ViewのログインでID入力欄をタップしただけでSIGABRTするのは私だけでしょうか?


■原因■

root view controllerが別にあるためview controller上で上記shouldAutorotate / suportedInterfaceOrientationsをいじったとしても、標準のUINavigationController等で上書きされてしまう。


■解決策■

私のプロジェクトでは、Storyboard上のUINavigationControllerからメインviewを呼び出していましたので、UINavigationControllerのサブクラスNavigationControllerを作り、上記shouldAutorotate / supportedInterfaceOrientationsを貼ります。その上で、Storyboard上のnavigation controllerのcustom classをNavigationControllerに変更します。これでうまく行きました。いやはや。



■Salesforce mobile SDK for iOSの場合■

上記Salesforce mobile SDKの場合、テンプレートから作成されるプロジェクトはAppDelegate.hのnewRootViewControllerでViewとUINavigationControllerを生成しています。

上記と同じ手順でUINavigationControllerのサブクラスNavigationControllerを作り、AppDelegate.mにインクルードしてから、newRootViewControllerを書き換えます(書き換え箇所は赤字)。


- (UIViewController*)newRootViewController {
    RootViewController *rootVC = [[RootViewController alloc] initWithNibName:nil bundle:nil];
    NavigationController *navVC = [[NavigationController alloc] initWithRootViewController:rootVC];
    [rootVC release];
    
    return navVC;
}



2013年7月8日月曜日

Salesforce mobile SDK for iOS 1.5

五十肩とか自転車レースとか転倒等によりすっかりご無沙汰してしまいました。今日はSalesforce mobile SDK for iOS 1.5のお話。

■感想:速い…?■

1.3当時は更新がうまいこと反映されないなどの問題があったので切っていたのですが、今回試してみたら起動がヤケに速い。記事によるとデフォルトでonになったとのこと。せっかくなので試してみたら、以前の問題は解消していて、これならキャッシュとして十分機能していると判断し、今回からONにしました。

Authentificateも早くなったような気がします。私のアホなアプリ「FourChatter」はあまり処理らしい処理をしていないのに(いないからこそ?)、2回目以降の起動時間が1/4ぐらいになりました。

■問題点■

ご存知の通り、iOS 6でOrientationの仕様が大きく変わっています。そのせいかどうか、SDKから作ったテンプレートアプリのinfo.plist上でPortrait Upside Downに設定しても、upside downになってくれません。他の方向へは問題なくrotateしますし、UIDeviceOrientationDidChangeNotificationでnotificationを受けるとちゃんと「upside downになるで」と通知が来ますが、表示は横を向いたままです。

追記:解決しました→古いアプリをiOS6対応にしたらupside downにならない件を解決

SFNativeRestAppDelegate.mを見ると「iOS 6になったらこの書き方はダメ」って書いてある書き方が多々あるけど、うーむ…。

かといってiOS 5でビルドするとSalesforceへのログイン画面に触っただけで落ちるし…困ったもんだ。

■そこに別の問題が■

アップデートしたアプリをAppleへ送付しようとしたのですが、失敗しました。Validate...は成功しますが、Distributeでエラーが…。


ググるとJVM関連のトラブルのようで、JDK 6が立ち上がるようにすれば良いという解決策などが出ているのですが、ダメでした…。この件、アップルに問い合わせメールを送って対応待ちです。いやはや。

2013年6月1日土曜日

Fitbit oneのリストバンドが朝になると外れている件、を解決

NHK技術研究所公開中ですね。玄関脇のIEEE記念看板
睡眠を記録するためにFitbit oneを入れて手首に装着するリストバンド、ふと夜中に目を覚ますと必ず外れている。

ベルクロがちょっとはみ出した部分が服や布団に張り付いて、バリバリ→やめてー、になっている模様。

はみ出ないようにきちっと巻いたり、ベルクロの位置を調整したり工夫してみたけど、ダメ。そのうち、めんどくさくなってしなくなっていまして。

が、昨夜久しぶりに装着してみたのですが、ふと思いついて自由に回転するようにユルユルに巻いてみた。

解決しました。
実際は2:30起床で止め忘れた。それ以前に睡眠の質が悪すぎるorz

結論:Fitbit oneのリストバンドはゆるく装着しましょう。

2013年5月26日日曜日

SalesforceからiPhoneへPush notificationを送る

とんかつ専門店のご飯が妙に美味いのはなぜ?
上の写真はまったく関係ありません。

週末、何かネタを探していたら、www.parse.comというサービスに関する記事が見つかりました。最近ではPush Notification代行サーバもいろいろあるのですが、これは余計な機能がないぶんシンプルで安い。

そんなわけで、chatter上に「$go message」と書くと、triggerが作動してForce.comのhttp calloutからparse.comのAPIを叩き手元のiPhoneにメッセージが届く、というのを作ってみました。

■まず、Push Notificationを試す■

試すと言いますか、こちらの記事を参考に、いや、そのまま実行してみます。

Remote Push Notification ASPサービスを試す

Parse、一発で動いてPushされました。いやー、いままでこんなに簡単にPushできたのって初めてかもしれない。10分か15分くらいで動いてしまいました。



■FeedItemのTriggerからParseのREST APIを呼ぶ■

さて、ここからはForce.com上での作業。TriggerからREST APIを呼ぶことはできないので、Triggerからバッチを起動、バッチからcall outする作戦。

忘れないうちに、まずはお約束のリモートサイトの設定。
  管理者設定>セキュリティのコントロール>リモートサイトの設定
これでhttps://api.parse.com/を登録しておきます。

■Batchを作る■

Developer ConsoleからClass CallParseを定義、IterableなBatchApexでParse.comのAPIを叩きます(コードは末尾)。

■Triggerを作る■

例によってDeveloper ConsoleからFeedItem上のキーワード $go を検出してBatch Apexを呼び出すコードを書きます(コードは末尾)。

■動かない■

一応、Salesforce上で
  管理者設定>監視>デバッグログ
を設定してから、Chatter上にメッセージを書いてみる。

…動かない。デバッグログを見ると、

05:50:02.039 (39869000)|CALLOUT_REQUEST|[29]|System.HttpRequest[Endpoint=https://api.parse.com/1/push, Method=POST]
05:50:02.175 (175302000)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:661
05:50:02.175 (175393000)|CALLOUT_RESPONSE|[29]|System.HttpResponse[Status=Bad Request, StatusCode=400]
05:50:02.175 (175420000)|HEAP_ALLOCATE|[29]|Bytes:130
05:50:02.175 (175438000)|SYSTEM_METHOD_EXIT|[29]|System.Http.send(ANY)

と出ていました。ちくしょう、どうせ一発で動くと思ってねぇですよ←涙目 なので、Apex側にコードを入れて
    System.debug(res.getBody());
    System.debug(res.getStatus());
    System.debug(res.getStatusCode());
検証してみたところ
    16:43:08.153 (153194000)|USER_DEBUG|[36]|DEBUG|{"code":107,"error":"This endpoint only supports Content-Type: application/json requests, not : application/json."}
という結果が。

ソースを改めて見なおしたら、

    req.setHeader('Content-Type : ', 'application/json');

なんてバカなコーディングを発見。直したら無事動作。




以下コードを張っておきます。iPhone側はparse.comのサンプルを貼っただけです。

■Trigger■


trigger KickFromChatter on FeedItem (before insert) {

    FeedItem[] feeds = Trigger.new;
    List<FeedItem> arrayFeedItem = new List<FeedItem>();
    
    for (FeedItem feed : feeds) {
        String strBody = feed.Body;
        if (strBody != null && strBody.contains('$go')) {
            arrayFeedItem.add(feed);
        }
    }
    
    if (arrayFeedItem.size() > 0) {
        CallParse batch = new CallParse();
        batch.arrayFeedItem = arrayFeedItem;
        
        Database.executeBatch(batch);
    }
}


■Batch Apex■


global class CallParse implements Database.batchable<FeedItem>, Database.AllowsCallouts {
    
    global List<FeedItem> ArrayFeedItem;
    
    global Iterable<FeedItem> start(Database.BatchableContext info) {
        Iterable<FeedItem> i = arrayFeedItem;
        
        return i;
    }
    
    global void execute(Database.BatchableContext info, List<FeedItem> feeds) {
        for (FeedItem feed : feeds) {
            String str = feed.Body;
            String strPayload = '{"channels" : [""], "data" : {"alert" : "' + str + '"}}';
            Blob blobPayload = Blob.valueOf(strPayload);
            
            // Instantiate a new http object
            Http h = new Http();
            
            HttpRequest req = new HttpRequest();
            req.setEndpoint('https://api.parse.com/1/push');
            req.setMethod('POST');
            
            req.setHeader('X-Parse-Application-Id', 
                                   'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
            req.setHeader('X-Parse-REST-API-Key',
                                   'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
            req.setHeader('Content-Type',
                                   'application/json');
            
            req.setBody(strPayload);
            
            // Send the request, and return a response
            HttpResponse res = h.send(req);
            
            System.debug(res.getBody());
            System.debug(res.getStatus());
            System.debug(res.getStatusCode());
        }
    }
    
    global void finish(Database.BatchableContext info) {
    }
    
}


2013年5月8日水曜日

Force.com:添付ファイルの全文検索

ChatterにMS-Officeのファイルをアップロードし、ブラウザでログインして「検索」から探すとファイルの中身をちゃんと探してくれるのですが、APIからFINDを使ってもヒットせず苦労していましたが、解決策が見つかったのでメモ。

■ContentVersionにあった■

Chatterにアップロードしたファイルを全文検索で探し出したい。

とりあえず、ファイルがどこに格納されているかをコンソールでSOQLで探したところ、ContentVersionにそれらしいレコードが見つかりました。その結果を踏まえてEnterprise/WSCで以下のSOSLを実行したところ、うまく検索してくれました。

  FIND {語句} RETURNING ContentVersion (Id, FirstPublishLocationId, IsLatest, PathOnClient)

ここで、IsLatest = trueが最新版を示すフラグ、PathOnClientはファイル名です。FirstPublishLocationIdには関連するオブジェクトのIDが入っているので、たとえばChatterからアップロードした場合には、
  SELECT Id, Body FROM FeedItem WHERE Id = cv. FirstPublishLocationId
てなことを書けば探し出せます。

■試行錯誤■

何もパラメータなしに
  FIND {語句} 
とすると一応全ファイルが対象になるはず。そうでなくとも
  FIND {語句} RETURNING FeedItem (Id, Body, CreatedDate)
などと書けばヒットする、はず。

でも、どっちもダメでした。

それとPDFもダメでした。日本語のPDFはほぼ全滅、英語版PDFもヒットしないことの方が多いという状況です。いろいろな文書ファイルで試してますが、Office 2007ファイルについては今のところ漏れなくヒットします。

マカーの私としては、PDFが対象外ってのはちょっと困るのですが…。

以上、ご参考になれば幸いです。

2013年5月6日月曜日

「倉橋浩一」

でググると、今のところは私の記事がトップに来るけど、日本大学に同姓同名の人がいるな。Facebookでは同じ読みの人が沢山出てくる。

そろそろ何かアウトプットしないといけないなぁ…。

■太っていてびっくり■
OOエンジニアの輪! ~ 第 23 回 倉橋 浩一 さんの巻 ~

■バカというのは治らない病気■
倉橋浩一@WebObjectsは、YS-11を心より愛す|【Tech総研】

■懐かしい…。■
じつはWebObjectsで飯食ってます(連載一覧)

■私の前が林信行さんで次が竹林賢さんってスゲエ■
林檎いとしや Vol.66 倉橋浩一 人生への投資効果

2013年4月18日木曜日

apex:actionSupportについて

小さなネタですが。昨日ひさしぶりにVisualforceを使いました。

apex:pageBlockTableの中でapex:actionSupportを使ったのですが、event="OnRowClick"に反応してくれません。apex:commandLinkに置き換えてactionで呼び出されるメソッドに問題がないことは確認済ですが、それ以前の問題として全然イベントに反応している様子が見られない。

よくよくReferenceを見なおしたらOnRowClickではなくonRowClickとある。

…動きました。えー、case sensitiveなのかよー …と八つ当たりした午後3時でした。

関係ないけど、Apexの大文字と小文字の扱いにところどころ違和感を覚える。まぁ昔と違って、

    public String fullname {get; set;}

と書くとgetter/setterがうまく認識されない、というバグだか仕様は治ったみたいだけど。