UIWebView で history.back が使えないケース

このたびの東北太平洋沖地震で被災されているみなさん、ご家族、ご友人のみなさんに、心からお見舞い申し上げます。

非常に月並みな言葉ではありますが、私も、被災地で生まれ育った関係者のひとりとして、今はもう感情に訴えるべきでないという思いがあるので、このくらいにさせてください。ただ、ひとりのエンジニアとして、ひとりのビジネスマンとして、ひとりの日本人として、被災地の社会・経済の復興に役立つような、実利ある行動を取っていきたいと思っています。

さて。

『実利ある行動』とか銘打ったあとにやりづらいですが、小さい話をします。iPhone の UIWebView で表示したサイトで、 javascript:history.back() が動かないケースがあってハマりました。

結論から言うと、UIWebView クラスに loadData:MIMEType:textEncodingName:baseURL: や loadHTMLString:baseURL: でロードした HTML ページは、javascript の履歴スタックに載らないようです。

これらのメソッドでロードしたページから次のページに遷移したあと、そこで history.back() を動かしても、うんともすんとも言いません。UIWebView には、canGoBack というブラウザの戻る機能が使えるかどうかを判別するプロパティや、goBack というブラウザの戻る機能そのものとなるメソッドもあるのですが、これらも効きませんでした。とにかく、UIWebView が直接 HTTP 経由で受信したデータ以外は、履歴をたどれません。

細かい通信制御がしたかったため、NSURLConnection でデータを取得して、それを UIWebView の loadData:MIMEType:textEncodingName:baseURL: に食わせるようにしていたのですが、この設計だと history.back() が使えないわけです。分かるか、そんなもん。

ということで、UIWebView で history.back() を使いたい場合は、loadRequest: メソッドで HTML を読み出しましょう。細かい制御は、UIWebViewDelegate でごにょりましょう。

1時間以上ハマりました。相変わらず Cocoa に慣れない。

View+Navigation based iOS アプリの作り方

Xcodeで iOS アプリの新規プロジェクトを作成する際、Navigation-based ApplicationやView-based Applicationといったテンプレートを選択することができますが、その両方の機能性を持たせたいということがあると思います。というか、さっきそういう境遇に陥って困ったので、メモです。

今回は、アプリのトップ画面は View-based Application だけれども、そこからメニューを選んだときには NavigationController のスタックに積んで、Navigation Bar 付きのページに遷移していくというもの。うむ、説明が難しい。平たく言うと、トップ画面は Navigation Bar がなくて、2階層目の画面は Navigation Bar があってトップに戻れる感じです。

さっそく作ります。ここでは、Sampleというアプリを考えます。

まず、View-based Application テンプレートを使ってプロジェクトを作成します。これに NavigationController の機能を追加していきます。

はじめに SampleAppDelegate.h を開いて、UINavigationController を追加します。1行追加するだけ。

@interface SampleAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    UINavigationController *navigationController;
    SampleViewController *viewController;
}

次に SampleAppDelegate.m を開いて、application:didFinishLaunchingWithOptions: と dealloc を以下のように修正します。NavigationController の rootViewController に、デフォルトの UIViewController をセットして、その view を window のサブビューに追加して表示させるようにしているのがポイントでしょうか。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    
	navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    
    // Add the view controller's view to the window and display.
    [window addSubview:navigationController.view];
    [window makeKeyAndVisible];
    
return YES; }
- (void)dealloc { [viewController release]; [navigationController release]; [window release]; [super dealloc]; }

続いて、トップ画面では NavigationBar を隠し、2階層目以降は表示するようにします。ここでは next: が、2階層目を開く処理として、ボタンか何かに紐付けた IBAction 的なメソッドということにします。

SampleViewController.m を開いて、以下のような感じにします。

#import "SampleViewController.h"
#import "NextViewController.h"
    
@implementation SampleViewController
    
- (void)viewWillAppear:(BOOL)animated {
	self.navigationController.navigationBarHidden = YES;
}
    
- (void)hoge:(id)sender {
	NextViewController *next = [[NextViewController alloc] init];
	[self.navigationController pushViewController:next animated:YES];
 	[next release];
	self.navigationController.navigationBarHidden = NO;
}

// 以下略

ここでのポイントは、 navigationBarHidden に BOOL オブジェクトをセットするタイミングです。pushViewController:animated: する前に NavigationBar を表示しようとすると、画面遷移前ににょきっと NavigationBar が現れてくるので、あんまり心地良くありません。いろいろ試してみていただければと思いますが、コードを書く位置で、目に見える違いがあるのが GUI 開発の面白いところですね。

これで目的達成です。おつかれさまでした。大したことないですが、そのうち github にでも上げときます。

UIModalTransitionStyleCoverVertical のようなアニメーション

UIViewController でモーダルビューを表示するメソッド presentModalViewController:animated: の画面遷移アニメーションのスタイルは、デフォルトだと画面下からビューがせせり出てくる UIModalTransitionStyleCoverVertical になっています。結構気持ち良いので、これと同じアニメーションを、自分の UIView でも実装してみるテスト。というか、大人の事情により途中まで書いていてボツになったので、もったいなので、ここで書き上げてみます。

UIViewController内で UIView をモーダルで表示させたいメソッド内で、次のようなアニメーションを組めばOKのはず。

UIView *myView = [[UIView alloc] init];
/*
  View 生成する処理むにゃむにゃ
 */

// 初期位置を決める CGPoint p; if (self.interfaceOrientation == UIInterfaceOrientationPortrait || self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) { p = CGPointMake(0.0, CGRectGetHeight([[UIScreen mainScreen] applicationFrame])); } else { p = CGPointMake(0.0, CGRectGetWidth([[UIScreen mainScreen] applicationFrame])); } CGRect r = myView.frame; r.origin = p; [myView setFrame:r];
// Animation CGContextRef context = UIGraphicsGetCurrentContext(); [UIView beginAnimations:nil context:context]; [UIView setAnimationDuration:0.5]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; CGRect newRect = myView.frame; newRect.origin = CGPointMake(0, 0); [myView setFrame:newRect]; [UIView commitAnimations];
[self.view addSubview:myView]; [myView release];

ちなみに、UIModalTransitionStyleCoverVertical の animationCurve は UIViewAnimationCurveLinear かもしれません。個人的には EaseInOut が一番気持ちいいのでそれにしているだけです。


最新エントリー
UIWebView で history.back が使えないケース
View+Navigation based iOS アプリの作り方
UIModalTransitionStyleCoverVertical のようなアニメーション
あわせて読みたいブログパーツ