NSStringの空チェック

NSStringの空チェックはnilのチェックと @”" の2つで判定してる人もいるかと思います。
ただ、ちょっと大げさな気もします。

NSString *string = @"Hello!!";
if(string == nil || [string isEqualToString:@""]){
  NSLog(@"空です");
}

- (NSUInteger)length で 0 かどうかを判定したら楽です。nilが入ってても大丈夫です。

NSString *string = @"Hello!!";
if([string length] == 0){
  NSLog(@"空です");
}
カテゴリー: NSString, Objective-C | 2件のコメント

プログラム大喜利

久しぶりにRubyでやるか
えっと、文字列の追加ってどうだっけ?
えーと正規表現…こうだっけ?
とかそんなレベル

def output(string = "")
  # 9文字だったら終わり
  return string if string.length == 9

  # aからzまで繰り返し
  range = 'a'..'z'
  range.each do |x|
    s = string + x

    # 3文字連続があったらcontinue
    next if /#{x+x+x}/ =~ s

    # 再帰で呼び出し
    puts output(s)
  end
end

output

合ってるのか?これ

いいだしっぺの例はこちら http://d.hatena.ne.jp/big-bros/20111217

カテゴリー: 雑記 | コメントをどうぞ

stringByEvaluatingJavaScriptFromString: の可読性を改善する

UIWebViewの stringByEvaluatingJavaScriptFromString: は Javascript を実行するので何かとお世話になるメソッドです。
このメソッドの引数は NSString で Javascript のコードを書くことになるので長くなりがちです。
そこで、改行して見やすくすると激しくエラーが出ます。

// 1から10まで足した合計を出力
// エラーが出過ぎてビルドできる気配がない
[self.webView stringByEvaluatingJavaScriptFromString:@"
  var sum = 0;
  var N = 10;
  for (var i = 1; i <= N; i++) {
    sum += i;
  }
  alert('合計' + sum);
"];

Javascriptを別ファイルにしてそれを読み込めばいいのですが、ちょこっと動作を試したいためだけに別ファイルにするのは嫌じゃ!という方は2通り方法があります。

1. 改行する時にバックスラッシュ( option + ¥ )を挿入

// 1から10まで足した合計を出力
[self.webView stringByEvaluatingJavaScriptFromString:@"\
  var sum = 0;\
  var N = 10;\
  for (var i = 1; i <= N; i++) {\
    sum += i;\
  }\
  alert('合計' + sum);\
"];

2. 各行を”(ダブルクォーテーション)で囲む

// 1から10まで足した合計を出力
[self.webView stringByEvaluatingJavaScriptFromString:@""
  "var sum = 0;"
  "var N = 10;"
  "for (var i = 1; i <= N; i++) {"
  "  sum += i;"
  "}"
  "alert('合計' + sum);"
];

これで見やすくなりました。

カテゴリー: NSString, Objective-C, UIWebView | コメントをどうぞ

ARC環境下で非公開APIを実行する

ARCがオフの時には警告が出るだけでしたが、ARC環境下ではプロトタイプ宣言していないメソッドを呼ぶとビルド時にエラーになります。
これはカテゴリなりで宣言すればビルドは通ります。
(参考:ビュー (UIView) の階層構造をダンプする非公開の便利メソッド

- (NSString *)recursiveDescription;を実行する場合は以下のように書きます。

ViewController.h
#import <UIKit/UIKit.h>

// UIWebViewの非公開API用
@interface UIWebView (privateAPI)
- (NSString *)recursiveDescription;
@end

// 元のinterface部
@interface ViewController : UIViewController <UIWebViewDelegate>
{
...

------------------------------------------------

ViewController.m
#import "ViewController.h"

- (void)viewDidLoad
{
    [super viewDidLoad];

    // UIWebViewの非公開API recursiveDescription を呼ぶ
    NSLog(@"%@", [self.webView recursiveDescription]);
    ...
}

もっと短くしたい!というせっかちな方は
UIWebViewでデバッグツールを使うエントリでやってましたが、performSelectorを使えばエラーは出ません。

Before :

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  // 参考にしたサイトにはこう書いてあった。
  // ARCがONだとエラー
  [NSClassFromString(@"WebView") _enableRemoteInspector];

  ...
}

After :

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  // 非公開APIなのでデバッグが終わったらコメントアウトするべし!!
  // performSelectorだとビルドが通る。
  [NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];

  ...
}
カテゴリー: Objective-C | コメントをどうぞ

UIWebViewでUIScrollViewDelegateを利用する

iOS5で正式なプロパティとしてUIScrollViewが使えるようになりました。
なら、UIScrollViewDelegateも使いたくなります。
iOS5.xだけならば簡単なのですが、iOS4.xも対応させるとちょっと癖があります。

※ iOS4は非公開APIを使用するため場合によっては審査が通らない可能性があります。
※※ 今のところ、この方法でRejectされたことはありません。

inderface部はこんな感じ

@interface ViewController : UIViewController <UIWebViewDelegate, UIScrollViewDelegate>
{
    __unsafe_unretained IBOutlet UIWebView *_webView;
    __unsafe_unretained UIScrollView *_subScrollView;
}
@property (unsafe_unretained, nonatomic) UIWebView *webView;
@property (unsafe_unretained, nonatomic) UIScrollView *subScrollView;
@end

viewDidLoadでUIWebView内のUIScrollViewを取得し、Delegateを設定します。

- (void)viewDidLoad
{
    [super viewDidLoad];

    // UIWebView内のUIScrollViewを取得
    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 5.0) {
        for (UIView *subview in [self.webView subviews]) {
            if ([[subview.class description] isEqualToString:@"UIScrollView"]) {
                self.subScrollView = (UIScrollView *)subview;
            }
        }
    }else {
        self.subScrollView = (UIScrollView *)[self.webView scrollView];
    }

    // UIScrollViewのDelegateにselfを設定
    self.subScrollView.delegate = self;

    // ページ読み込み
    NSURL *url = [NSURL URLWithString:@"http://www.google.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    [self.webView loadRequest:request];
}

ここまででiOS5.xでは問題なく動作します。UIScrollViewDelegateでよく使いそうなメソッドで試してみます。
スクロールしたりステータスバーをタップするとログが出力されるかと思います。

- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
    NSLog(@"%s", __func__);
    return YES;
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
    NSLog(@"%s", __func__);
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    NSLog(@"%s", __func__);
    NSLog(@"contentOffset : %@", NSStringFromCGPoint(self.subScrollView.contentOffset));
}

しかし、このままではiOS4.xで拡大縮小ができません。
勘で試した所、UIWebViewの同一名のメソッド(非公開)で拡大縮小に関する処理がされているようです。
もう少しコードを追加します。

// iOS4.xでは下記メソッドを調整する (iOS5.xではあっても無くてもよい)
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return [self.webView viewForZoomingInScrollView:scrollView];
}

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
    [self.webView scrollViewWillBeginZooming:scrollView withView:view];
}

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
                       withView:(UIView *)view
                        atScale:(float)scale
{
    [self.webView scrollViewDidEndZooming:scrollView withView:view atScale:scale];
}

- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    [self.webView scrollViewDidZoom:scrollView];
}

これでiOS4でもUIWebViewのUIScrollViewDelegateが使えます。

サンプルをGitHubに置きました。
Google ニュースをホームページにしているので拡大縮小を試したい場合はiPhoneに最適化されてなさそうな記事へアクセスしてください
https://github.com/MasaruSakai/SampleWebViewWithUIScrollViewDelegate

カテゴリー: Objective-C, UIWebView | コメントをどうぞ

UIWebViewでFirebugみたいなツールを使う

iWebinspectorは素晴らしいのですが、どちらかと言えばWeb開発向けです。
UIWebViewを見たいですよね。
iOS5でなければいけませんが、やり方がありました。今度はiWebinspectorも入りません。
(参考:Nathan de Vries Enabling Remote Debugging via Private APIs in Mobile Safari

UIWebViewを使ってるアプリのAppDelegate の application:didFinishLaunchingWithOptions: で
以下のコードを追加します。

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

  // 非公開APIなのでデバッグが終わったらコメントアウトするべし!!
  [NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];

  ...
}

これで準備完了です。後は次のようにやっていけばOK
1. iOS5のシミュレータ上でアプリを実行する
2. UIWebViewに好きなページを表示させる
3. MacのSafariを立ち上げる
4. localhost:9999 にアクセスする
5. Page Listting に今表示してるページが出ていることを確認しリンクをクリック
6. あとは煮るなり焼くなり

iOS5でのみ動作 + 非公開API なのでデバッグが終わったらコメントアウトを忘れないようにしましょう。

カテゴリー: Objective-C, UIWebView | コメントをどうぞ

UIWebViewのスクロールを速くする

UIWebViewのスクロールが遅くてイラつきます。
実はこれUIWebViewが重いのではなくて、そういう設定になっているだけです。
デフォルトでは UIWebView 内の UIScrollView の decelerationRate が UIScrollViewDecelerationRateFast に設定されています。
それを UIScrollViewDecelerationRateNormal に変更すれば良いです。
幸いiOS5からはUIWebView 内のUIScrollViewがプロパティとして追加されたため
以下のようにするだけでスクロールの滑りがUITableViewのデフォルトと同等になります。

    self.webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;

iOS4でも非公開ながらUIScrollViewが組み込まれているため変更できます。

    if ([[[UIDevice currentDevice] systemVersion] floatValue] < 5.0) {
        for (UIView *subview in [self.webView subviews]) {
            if ([[subview.class description] isEqualToString:@"UIScrollView"]) {
                self.subScrollView = (UIScrollView *)subview;
            }
        }
    }else {
        self.subScrollView = (UIScrollView *)[self.webView scrollView];
    }
    self.subScrollView.decelerationRate = UIScrollViewDecelerationRateNormal;

iOS5でscrollViewが正式なプロパティになったためか、
上記の方法で今のところRejectされたことはありません。

サンプルをGitHubに置いときました。
MasaruSakai / SampleQuickScrollWeb – GitHub

あ、Mobile Safariみたいに画像のあるページで引っかかりが無くなる方法は分かりません。
誰か知ってればこっそり教えてください...

カテゴリー: Objective-C, UIWebView | コメントをどうぞ

UIWebViewで文字コード変更する : iOS Advent Calendar 2011

2011年のiOS Advent Calendar 8日目です。
特にホットな話題でも無いのですが、UIWebViewで文字コードの変更です。

Stack overflowを検索するとHow to change the character encoding in UIWebView?こんなのがあります。
なるほど、NSURLConnectionで取ってきて、loadDataなりloadHTMLStringで読み込ませるようです。質問者はframe内の表示に困っているようですが、自分としてはframe内なんてどうでもいi

で、この方法には別の問題があって、UIWebViewはloadRequestでなければ”戻る”,”進む”が使えなくなります。
Javascriptのhistory.backも使えなくなります。ブラウザのリロードを押したのと同じ感じです。
(参考:dTBlog UIWebView で history.back が使えないケース
HTML5で History APIが強化されたため、ページが変わったならhistoryオブジェクトに正しく履歴が残るようにしたいものです。

なので、loadRequestをなるべく使ったサンプルがこれです。
NSURLProtocolを使う方法とNSURLCacheを使う方法の2通りを試してます。
MasaruSakai / EncodingsForUIWebView – GitHub

1. NSURLProtocolで頑張る
ごめんなさい。
NSURLProtocolの存在を知りませんでした。orz
(参考:iOS Advent Calendar 2011 5日目 / NSURLProtocolの使い方
やり方はNSURLProtocolのサブクラスCustomURLProtocolを作り、startLoadingで非同期でデータを取得します。
css, javascript, 画像ならCustomURLProtocolで扱わないようにします。
突貫工事のため、なんとなく動いたけど所々動作がおかしいです。
NSURLProtocolはURL loading system内なので、startLoadingでNSURLConnectionを使うと再びNSURLProtocolに飛び込んでくるためASIHTTPRequestを使ってます。

問題点はNSURLProtocolへの値の渡し方がよく分からない。AppleのドキュメントにはアプリケーションはNSURLProtocolのインスタンスは直接作っちゃいかんと書いてあるし。
今回は変更した値をAppDelegateを使って渡してます。
(参考:仕事人の開発日誌 iOSゆとりプログラミングのススメ
他にはstatic変数を使う?文字コード毎に違うURLスキームを使う?
タブブラウザを作ってタブ毎に文字コードを変えたいとか結構厳しい気が… 誰か教えてくださいorz

2. NSURLCacheで頑張る
元々用意していた方法で、比較的扱いやすい方法です。
(参考:Cocoaの日々 UIWebView のキャッシュ調査

やり方はNSURLCacheのサブクラスCustomURLCacheを作成し、propertyで変更したい文字コードの情報等を受け取れるようにしておきます。cachedResponseForRequest内で、同期でHTMLファイルを取得してNSCachedURLResponseを作り直して返します。
こっちもCustomURLCache内でNSURLConnectionを使うと、再びCustomURLCacheに飛び込んでくるためASIHTTPRequestを使用してます。
Webページで重いのは画像であるため、プレーンなHTMLファイルだけであれば同期通信しても問題は起きにくいですが、NSURLProtocolで出来るのであれば使わない方が良いと思います。

CustomURLCache.h

@interface CustomURLCache : NSURLCache
{
    NSString *_encoding;
    NSString *_userAgent;
}
@property (strong, nonatomic) NSString *encoding;
@property (strong, nonatomic) NSString *userAgent;
@end

CustomURLCache.m
@implementation CustomURLCache
@synthesize encoding;
@synthesize userAgent;

-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
    NSLog(@"%s", __func__);

    NSCachedURLResponse* cacheResponse  = nil;
    cacheResponse = [super cachedResponseForRequest:request];

    // 文字コードがAUTOならスルー
    if ([self.encoding isEqualToString:@"AUTO"]) {
        return cacheResponse;
    }

    // css, javascript, 画像ならスルー
    NSString *pathExtension = [[request URL] pathExtension];
    if ([pathExtension caseInsensitiveCompare:@"css"] == NSOrderedSame ||
        [pathExtension caseInsensitiveCompare:@"js"] == NSOrderedSame ||
        [pathExtension caseInsensitiveCompare:@"jpg"] == NSOrderedSame ||
        [pathExtension caseInsensitiveCompare:@"jpeg"] == NSOrderedSame ||
        [pathExtension caseInsensitiveCompare:@"png"] == NSOrderedSame ||
        [pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame)
    {
        return cacheResponse;
    }

    // 同期でHTMLファイルを取得
    NSURL *url = [NSURL URLWithString:[[request URL] absoluteString]];

    ASIHTTPRequest *asiRequest = [ASIHTTPRequest requestWithURL:url];
    [asiRequest setUserAgent:self.userAgent];
    [asiRequest startSynchronous];

    NSDictionary *responseHeaders = [asiRequest responseHeaders];
    NSLog(@"responseHeaders : %@", [responseHeaders description]);
    NSArray *contentTypeArray = [[responseHeaders objectForKey:@"Content-Type"] componentsSeparatedByString:@";"];
    NSString *contentType;
    if ([contentTypeArray count] >= 1) {
        contentType = [contentTypeArray objectAtIndex:0];
    }else {
        contentType = nil;
    }
    NSLog(@"contentType : %@", contentType);

    // Content-Typeがtext/htmlならNSCachedURLResponseを作り直す
    if ([contentType isEqualToString:@"text/html"]) {

        NSURLResponse* response = [[NSURLResponse alloc] initWithURL:request.URL MIMEType:contentType expectedContentLength:[[asiRequest responseData] length] textEncodingName:self.encoding];
        cacheResponse = [[NSCachedURLResponse alloc] initWithResponse:response data:[asiRequest responseData]];
    }

    return cacheResponse;
}
@end

次のメソッドを使って、ViewControllerからの読み込み前(viewDidLoadとwebView: shouldStartLoadWithRequest: navigationType:)に文字コードの情報を更新してます。

ViewController.m

- (void)setCustomCache
{
    NSLog(@"%s", __func__);

    // CustomURLCacheの準備
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* documentsDirectory = [paths objectAtIndex:0];
    NSString* diskCachePath = [NSString stringWithFormat:@"%@/%@", documentsDirectory, @"_CustomCache"];
    NSError* error;
    [[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath withIntermediateDirectories:YES attributes:nil error:&error];

    CustomURLCache *cache = [[CustomURLCache alloc] initWithMemoryCapacity: [[NSURLCache sharedURLCache] memoryCapacity] diskCapacity: [[NSURLCache sharedURLCache] diskCapacity] diskPath:diskCachePath];
    cache.userAgent = [self.webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
    cache.encoding = self.encoding;

    [NSURLCache setSharedURLCache:cache];
}

他にはページのReloadでうまく文字コードが変更できなかったため、ASIHTTPRequestで取得したデータをloadDataに流し込む方法も使ってます。

来年の今頃はこんなこと考えなくてもいいようになってるといいなぁ

カテゴリー: Objective-C, UIWebView | 1件のコメント