2013年8月11日日曜日

PDFサムネイル表示例



PDFのサムネイルを表示する一例です。

以下の実装ではページ数の多いPDFでは全ページのサムネイルを作り終えるまで応答がなくなるので操作性に問題があり、またサムネイル作成中にメモリ不足になっても回復不能でアプリが落ちてしまいます。この改善方法についてはPDFサムネイル表示改良版を参照してください。

手順概要
  • 次のクラスを追加する。
    • WebViewWithThumnails UIWebViewのサブクラス
      サムネイル画像表示機能を追加するWebView
    • PdfThumbnailSrollVewi  UIScrollViewのサブクラス
      WebViewWithThumnailsに追加するサムネイル用スクロールビュー
    • PdfImageView UIControlのサブクラス
      サムネイルを表示し、タップに応答するビュー
  • MyViewControllerのviewをWebViewWithThumnailsに変更
    • WebViewWithThumnailsのdelegateにMyViewControllerをセット
  • MyViewControllerにgoogleなどの検索画面を表示
  • MyViewControllerのwebView:shouldStartLoadWithRequest:navigationType:メソッドでURLをチェックし、pdfファイルの場合は処理をトラップ
  • PDFファイルダウンロードが完了したら
    • WebViewWithThumnailsにPDFを表示
    • WebViewWithThumnailsにPdfThumbnailSrollVewi追加
    • PdfThumbnailSrollVewiにサムネールイメージをセット
実装例

PDFファイルをダウンロードする手順はダウンロードファイルをNSDataで取得する方法を参照。
「受信終了」で呼ぶ [self  self loadPdf]以降を例示。

@implementation MyViewController
//PDFファイルダウンロード完了
- (void)connectionDidFinishLoading:(NSConnection*)connection;
{
    _connection = nil;
    [self loadPdf];
}

//受信終了で呼び、ダウンロードした_dataをWebViewにロードする。
- (void)loadPdf
{
    //PDFをUIWebViewに表示する
  [ (WebViewWithThumbnails *)self.view loadData:_data
                                                            MIMEType:@"application/pdf"
                                               textEncodingName:@"utf-8"
                                                              baseURL:nil];
}
@end

@interface WebViewWithThumbnails : UIWebView 
- (void)addThumbnailsWithData:(NSData *)pdfData;
@end

@implementation WebViewWithThumbnails
//オーバライド、superを呼んだ後にサムネールをセットする。
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)encodingName baseURL:(NSURL *)baseURL
{
    [self  removeThumbnailScrollView];
    [super loadData:data MIMEType:MIMEType textEncodingName:encodingName baseURL:baseURL];
    [self addThumbnailsWithData:data];
}

//PDF以外の場合に呼ばれる。thumbnailScrollViewをremoveする。
- (void)loadRequest:(NSURLRequest *)request
{
    [self  removeThumbnailScrollView];
    [super loadRequest:request];
}

//subviewsの中にThumbnailScrollViewオブジェクトがあればremoveする。
- (void)removeThumbnailScrollView
{
    for(UIView *v in self.subviews) {
        if ([v isMemberOfClass:[ThumbnailScrollView class]]) {
            [v removeFromSuperview];
            break;
        }
    }
}

//サムネールセット
- (void)addThumbnailsWithData:(NSData *)pdfData
{
    [self  removeThumbnailScrollView];
    //画面下にUIScrollViewを追加
    ThumbnailScrollView *thumbnailScrollView =
        [[ThumbnailScrollView alloc] initWithWebView:self height:80];
    //各ページサイズに合わせたThumbnailViewを追加
    [thumbnailScrollView addThumbnailViews:pdfData];
    //ThumbnailViewにイメージをセット
    [thumbnailScrollView setPdfThumbnailImage:pdfData];
}
@end

@interface ThumbnailScrollView : UIScrollView
- (ThumbnailScrollView *)initWithWebView:(UIWebView *)superview height:(float)height;
- (void)addThumbnailViews:(NSData *)pdfData;
- (void)setPdfThumbnailImage:(NSData *)pdfData;
@end

@implementation ThumbnailScrollView
{
    //イメージセットが完了するまで保持する。
    CGPDFDocumentRef _pdf;
}

//保持していた_pdfをreleaseする。
//setPdfThumbnailImageでイメージセット完了時にも実行しているが、
//その前にdeallocされる可能性があるのでここでも実行する。
- (void)dealloc
{
    if (_pdf) CGPDFDocumentRelease(_pdf);
    _pdf = nil;
}

//superviewにUIScrollViewを追加。frameはsuperviewに合わせて設定する。
- (ThumbnailScrollView *)initWithWebView:(UIWebView *)webView height:(float)height
{
    //画面下10pt上の位置に設定。高さはパラメータの値、幅はsuperviewと同じ。
    CGRect r = CGRectMake(0, webView.frame.size.height - height - 10,
                          webView.frame.size.width, height);
    self = [super initWithFrame:r];
    if (self) {
        [webView addSubview:self];
    }
    return self;
}

//ScrollView内に、PDFページに対応するThumbnailImageViewを配置する。
//ここではまだイメージはセットしないが、矩形領域は表示されるようになる。
- (void)addThumbnailViews:(NSData *)pdfData
{
    CGDataProviderRef provider =
        CGDataProviderCreateWithCFData((__bridge CFDataRef)pdfData);
    _pdf = CGPDFDocumentCreateWithProvider(provider);
    //providerは同じメソッド内でreleaseしないとメモリリークの原因になる。
    CFRelease(provider);
    
    int numPages = CGPDFDocumentGetNumberOfPages(_pdf);
    float x = 10;
    float h = self.frame.size.height;
    float pagePos = 0;
    UIScrollView *webScrollView = ((UIWebView *)self.superview).scrollView;
    for (int p  = 1; p <= numPages; p++) {
        //CGPDFPageRefはautoreleaseされるのでreleaseする必要なし。
        CGPDFPageRef pageRef = CGPDFDocumentGetPage(_pdf, 1);
        CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox);
        float w = pageRect.size.width * (h / pageRect.size.height);
        CGRect imageRect = CGRectMake(x, 0, w, h);
        ThumbnailImageView *v = [[ThumbnailImageView alloc] initWithFrame:imageRect pageNum:p pagePos:pagePos];
        v.alpha = 0.5;
        [self addSubview:v];
        //タップ時のアクションをセット
        [v addTarget:self action:@selector(jumpToPage:) forControlEvents:UIControlEventTouchUpInside];
        x += w + 10; //サムネイル間に10ptの間隔を置く。
        pagePos += pageRect.size.height * (webScrollView.frame.size.width-5) / pageRect.size.width;
        //contentSize調整
        self.contentSize = CGSizeMake(self.contentSize.width + w + 10, self.contentSize.height);
    }    
    [self setNeedsDisplay];
}

//配置したThumbnailImageViewにイメージをセット
- (void)setPdfThumbnailImage:(NSData *)pdfData
{
    size_t numPages = CGPDFDocumentGetNumberOfPages(_pdf);
    for(int p=1; p<=numPages; p++) {        
        CGPDFPageRef pageRef = CGPDFDocumentGetPage(_pdf, p);
        ThumbnailImageView *v = [self.subviews objectAtIndex:p-1];
        [v setImageWithPdfPage:pageRef];
    }
    //ループが無事完了したら_pdfをリリース
    CGPDFDocumentRelease(_pdf);
    _pdf = nil;
    [self setNeedsDisplay];
}

//タップされたサムネールのページ位置に移動
- (void)jumpToPage:(ThumbnailImageView *)sender
{
    UIScrollView *scrollView = ((UIWebView *)self.superview).scrollView;
    CGPoint point = CGPointMake(0, sender.pagePos * scrollView.zoomScale);
    [scrollView setContentOffset:point animated:YES];
}

@end

2013年7月2日火曜日

ダウンロードファイルをNSDataで取得する方法



htmlのリンクでダウンロードするデータをNSDataで取得する一例です。ここでは説明を簡単にするためにPDFファイルとしていますが、それ以外の場合にも適用できます。

手順概要
  • UIWebViewDelegateのwebView:shouldStartLoadWithRequest:navigationType:メソッドでURLをチェック
  • PDFファイルの場合はNOを返し、UIWebViewから直接リクエストを送らないようする。(YESを返しても動作するが同じファイルを同時にリクエストすることになる?)
  • NSURLRequestを使ったリクエスト送信のメソッドを非同期で実行する。
  • NSURLRequestの受信データをNSDataに蓄積する。
  • NSURLRequestの受信完了でNSDataを使用する処理を実行する
実装例

storyboardでMyViewControllerのviewのクラスをUIWebViewに変更
viewのoutlets delegateをMyViewControllerに接続

//MyViewControllerにUIWebViewDelegateプロトコルを追加
@interface MyViewController : UIViewController <UIWebViewDelegate>

//インスタンス変数
@implementation FirstViewController
{
    NSURLConnection *_connection;
    NSMutableData *_data;
}

- (void)dealloc
{
    [_connection cancel];
    _connection = nil;
    _data = nil;
}

- (void)didReceiveMemoryWarning
{  
    [_connection cancel];
    _connection = nil;
    _data = nil;
    [super didReceiveMemoryWarning];
}

//画面表示時に検索ページを表示
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSString *urlStr =
        [NSString stringWithFormat:@"http://www.google.co.jp/search?q=pdf"];  
    [self sendHttpRequest:[NSURL URLWithString:urlStr]];
}

//webView:shouldStartLoadWithRequest:navigationType:実装
- (BOOL)webView:(UIWebView *)webView
     shouldStartLoadWithRequest:(NSURLRequest *)request
                 navigationType:(UIWebViewNavigationType)navigationType
{
    //URLにPDFオープンパラメータが付いている場合を考慮
    NSRange range = [request.URL.absoluteString rangeOfString:@"\\.pdf(#.+|$)"
            options:NSRegularExpressionSearch|NSRegularExpressionCaseInsensitive];
    //PDFの場合はデータ取得
    if (range.location != NSNotFound) {
        [self sendHttpRequest:request];
        return NO;
    }
    //その他はUIWebViewにまかせる
    return YES;
}

//リクエスト送信
- (void)sendHttpRequest:(NSURLRequest *)request
{
//まだ受信が完了していない_connectionがあるかもしれないのでcancelしておく。
[_connection cancel];
    _connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    if (_connection) {
        //受信データを格納するNSData
        _data = [NSMutableData data];
    } else {
        [(UIWebView *)self.view loadHTMLString:@"接続エラー" baseURL:nil];
    }
}

//レスポンス受信 - 念のため_dataを空にする
//Multipartの場合に複数回呼ばれる可能性あり
- (void)connection:(NSURLConnection *)connection
        didReceiveResponse:(NSURLResponse *)response
{
     [_data setLength:0];
}

//受信データの結合 - 複数回呼ばれる
- (void)connection:(NSURLConnection *)connention didReceiveData:(NSData *)data;
{
    [_data appendData:data];
}

//エラー発生時に呼ばれる
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
{
    _data = nil;
    _connection = nil;
}

//受信終了
- (void)connectionDidFinishLoading:(NSConnection*)connection;
{
    _connection = nil;
    //PDFをUIWebViewに表示する
    [self loadPdf];


2013年6月20日木曜日

iPadでresignFirstResponderが機能しない



iPadでUIModalPresentationFormSheetを使ってModal Viewを表示した場合、UITextFiledのタップでキーボードを表示したあと、textFieldShouldReturn:でresignFirstResponderを行ってもキーボードが消えないという現象があります。
続けて入力を行う可能性を考慮し、ひとつのコントロールがresignFirstResponderしてもいちいちキーボードを隠さないという仕様だそうです。
それでも直ぐにキーボードを消したい場合はUIViewControllerに次のようなコードを追加します。

- (BOOL)disablesAutomaticKeyboardDismissal {
    return NO;
}

iPadでは画面構成によってはUIViewControllerがいくつも登場するため、どれにしかければ有効なのか悩まされました。
Modal Viewの場合はそれを表示するUINavigationControllerをサブクラス化し、それに上記メソッドを実装すると解決します。

参照:resignFirstResponder Don't work?

2013年6月14日金曜日

All tests did not finish


Xcodeでテストを実行すると"All tests did not finish"と警告がでる。
Application Testの場合、Application起動中にTestが終了してまうのが原因だそうで、書き込みどおりsleepさせてみたら警告がでなくなった。
Some of my unit tests tests are not finishing in XCode 4.4

ということで、テスト項目が少ないときにこの警告が出ても気にしなくてよいようです。

2013年6月10日月曜日

Base Localizationの追加


古いバージョンで作ったプロジェクトにBase Localizationの追加する手順

Projectを選択
Localizationの"Use Base Internationalization"をクリック。

ベースとなる言語を選択

Finishをクリック。


------------------------------
メニューからの操作する場合
 PROJECTを選択
 Editorメニューの"Add Localization"をクリック
 言語選択サブメニューの"Other" > "Base(Base)"をクリック
------------------------------

Base.lprojが追加され、en.lprojフォルダからstoryboardが削除され
infoPlist.stringsだけになる。

同様にEditorメニューの"Add Localization"で"Japanese"を追加する。
ja.lprojが追加される。
MainStoryboard.strings、Localizable.stringsを追加する。
MainStoryboardの部分はStoryboardと一致させる。
StoryboardのUI部品はMainStoryboard.stringsに、NSLocalisedStringで参照するものはLocalizable.stringファイルに設定する。
MainStoryboard.stringsは後述のibtool で作成するものでよい。

storyboadへUI部品を追加したときのstringsファイルの更新
Terminalを起動
Base.jprojへ移動
ibtool MainStoryboard.storyboard --generate-strings-file NewStuff.strings
NewStuff.stringsを開き、追加された行をMainStoryoard.stringsへコピーする。

毎回コマンドを入力するのは面倒なので、コマンドのaliasを作っておくと便利です。
alias設定の方法は"mac osx terminal alias"などで検索してみてください。
例:
alias newstrings="cd (プロジェクトのパス)/Base.lproj;lbtool Mainstoryboard_iPhone.storyboard --generate-string-file new.strings"

参照:
Internationalize Your App


2013年6月9日日曜日

iOS WorkspaceでFramework(Library) Projectを作る方法



XCodeでWoekspaceを作り、Applicarion ProjectとFrameowork Projectを関連づける方法です。
検索するといくつも出てはきますが、バージョンによって手順が異なっており、XCode 4.6.1にズバりのものが見つかりません。
一番近かったのはIntroduction to Using Static Libraries in iOSで、これは既存プロジェクトからFrameworkを分離する方法ですが、これに沿った手順で次の要領で新規に作ることができます。
  1. 新規Workspaceを作る。


  2. Frameworkプロジェクトを作る。

    Workspaceに追加する前にプロジェクトを閉じる。
  3. WorkspaceにFrameworkプロジェクトの.xcodeprojを追加する。
    File>New>またはWorkspaceの左ペイン右クリックからAdd FIles to ...をクリック。
     

    同様にApplicationプロジェクトの.xcodeprojを追加する。
  4. Workspaceで各々のプロジェクト内のtargetを選択、Workspaceでbuildする。
    正しくbuildされれるとTestFrameworkの下のproduct内のlibTestFramework.aの赤字が黒に変わる。赤のままのときはWorkspaceのProject選択をチェック。もし別にTestFrameworkプリジェクトが開いたら閉じる。
  5. Workspace でApplicationプロジェクトのtargetを選択。Build Phase>Link Binary With LibrariesでFrameworkの.aを追加する。



  6. Framework内のクラスを使うファイルに#importを追加。
    "TestFramework/TestFramework.h"
    "TestFramework"はtargetの"Product Name"に設定されている名前。"TestFramework.h"は使用するクラスのヘッダファイル。

  7. Build Phases 公開するヘッダの設定
    プロジェクト作成時に追加されるヘッダは予め含まれているが、その後プロジェクトに追加するヘッダファイルを公開する場合は、手動で追加する。
Framework Project のBuild Setting リンカオプション(-ObjC)は自動的に設定されますが、Application Projectには設定されていません。このままでも問題に場合が多いと思いますが、Framework に Category が含まれている場合はApplication Projectにもこのオプションを追加する必要があります

 "selector not recognized" が発生する場合は多分これが原因です。

Building Objective-C static libraries with categories



2013年6月8日土曜日

uncaught exceptionのstackのアドレスをシンボル表示する方法



たまにstackのアドレスだけがありシンボル表示がなく、どこで発生しているエラーかわからないメッセージで終了することがあります。

エラーメッセージの例:
2013-06-08 14:27:49.160 Test[29884:11303] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM removeObjectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:

(0x1c90012 0x10cde7e 0x1c321c4 0x20bb 0xacb5b3 0x1c4f376 0x1c4ee06 0x1c36a82 0x1c35f44 0x1c35e1b 0x1bea7e3 0x1bea668 0x11ffc 0x1b4d 0x1a75)

こんな場合にuncaught exceptionの内容を表示する方法が紹介されていました。
Xcode 4.2 debug doesn't symbolicate stack call

次のコードはこのページからの引用です。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{
    .....
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
}


void uncaughtExceptionHandler(NSException *exception) {
    NSLog(@"CRASH: %@", exception);
    NSLog(@"Stack Trace: %@", [exception callStackSymbols]);

}

出力例:
2013-06-08 14:27:49.153 Test[29884:11303] Stack Trace: (
0   CoreFoundation                      0x01c9002e __exceptionPreprocess + 206
1   libobjc.A.dylib                     0x010cde7e objc_exception_throw + 44
2   CoreFoundation                      0x01c321c4 -[__NSArrayM removeObjectAtIndex:] + 212
3   Test                                0x000020bb -[MyViewController remove] + 91
4   Foundation                          0x00acb5b3 __NSFireDelayedPerform + 380
.....
)

エラーによっては手がかりになる情報も含まれてきます。仕込んでおいて損はないでしょう。

追記
swiftの場合
        NSSetUncaughtExceptionHandler{ exception in
            print(exception)
            print(exception.callStackSymbols)

        }
参照リンク:

How should I use NSSetUncaughtExceptionHandler in Swift


2013年4月19日金曜日

iOS Developer Program 更新後の作業

iOS Developer Program 更新後の作業
  1. Apple Developer Program Activation Codeが届く
  2. メールのアクティベーションコードをクリックし、Activation手続きを実行
  3. Provisioning Portalへ
    >Member Center
    >Certificates, Identifiers & Profiles
    >iOS Provisioning Profiles
  4. Certificate追加
    ダウンロードし、ダブルクリックでキーチェーンに追加
    更新前の同名のCertificateをキーチェーンから削除する。
  5. Provisioning Profile追加
    ダウンロードし、XCodeにドラッグ&ドロップ
関連エラー
  1. Certificate identity 'iPhone Developer: MyName (XXXXXXXXXX)' appears more than once in the keychain. The code sign tool requires there only be one.

    更新前の同名Certificateがキーチェーンに残っていると発生。

  2. Provisioning Profile 'MyApp Development' specifies the Application Identifier 'MyApp' which doesn't match the current setting 'MyApp'Provisioning Profiles

    Certificate、Provisioning Profileの一方だけが更新されている場合に発生。

  3. No unexpired provisioning profiles found that contain any of the keychain's signing certificates

    build対象のdeviceのprovisioning profilesに有効なものがない。
    • simulatorが対象の場合はOKだが、iPhoneなどの外部deviceの場合にエラーが発生する場合
      Organizerで当該deviceのprovisioning profilesをチェックする
      不要なものを削除、必要なものを追加する
    • buildできるアプリとできないアプリがある場合
      ProjectのBuildSettingでCode Settingをチェック
      選択されているprofileのApp IDをProvisioning Portalでチェック
      それに当該アプリのApp IDがマッチしているかチェック
      当該アプリのApp IDにマッチするprovisioning profileを更新、または追加する
      ProjectのCode Settingの設定が自動の場合、それまで選択されていたprofileが削除または無効になった場合に有効なものに置き換えられ、そのApp IDが当該アプリにマッチしなくなることがある。
  4. The entitlements specified in your application’s Code Signing Entitlements file do not match those specified in your provisioning profile. (0xE8008016).

    何らかの理由でアプリに適用されたProvisioningでは対象Deviceにアプリを転送することが許されていない場合、といえるようです。

    私の場合は追加したProvisioningにエラーが発生するdeviceを含めてなかったため発生しました。
    特定のDeviceでだけ発生するような場合は、Provisioningの設定にそのDeviceが含まれているかチェックしてみてください。

    様々な理由があるようで、いくつかの対処策が書き込まれています。
    http://stackoverflow.com/questions/5240801/the-entitlements-specified-profile-0xe8008016-error-ios-4-2

    よくわからない場合はアプリに適用されているProvisioningをいったん削除し、再度作り直してみるのが手っ取り早そうです。

2013年3月20日水曜日

Undefined symbols for architecture armv7: "_CMTimeMake", referenced from:



CMTimeMakeを使おうとしたら次のエラーが発生しました。
Undefined symbols for architecture armv7:  "_CMTimeMake", referenced from:

CoreMedia frameworksを追加すると解消します。

2013年3月17日日曜日

Interface Builder was unable to determine the type of ”xxxxx.storyboard“



変なエラーに遭遇しました。
Interface Builder was unable to determine the type of ”xxxxx.storyboard“

結果的には、そのstoryboardが入っているlprojフォルダをいったんdelete=>Remove Referencesで削除し、Add Filesで再度プロジェクトに追加することで解消しました。
snapshotで以前の状態にもどしても同じエラーが発生しました。どうもXcodeがコンパイル対象としている内容とソースコードが違っているように思えるのですが、よくわかりません。

2013年3月2日土曜日

システム定義済みのドキュメントタイプ(UTI)登録


アプリケーションがオープンすることのできるファイルの種類は、plistにUTI(Uniform Type Identifiers)を登録することで設定できます。

mp3、m4aを登録する場合は、plistをProperty Listとしてオープンし、次のように設定します。

この設定を行うとm4a再生アプリ選択肢にこのアプリが追加されます。
例えばメールに添付されたm4aファイルをプレスするとファイルを開くアプリを選択することができますが、これに自分のアプリが含まれるようになります。

上記の設定のplistのSource Codeは次のようになります。
<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>LSItemContentTypes</key>
        <array>
            <string>public.mpeg-4-audio</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>Audio</string>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>m4a</string>
        </array>
        <key>LSHandlerRank</key>
        <string>Default</string>
    </dict>
</array>

外部アプリから起動またはアクティブにされた場合は、UIApplicationDelegateの次のメソッドが呼ばれので、このファイルをオープンするように準備をします。

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation;

定義済みUTIリスト



2013年2月16日土曜日

Could not instantiate class named NSLayoutConstraint



iOS SDK 6のstoryboardを使用したアプリをiOS 5で動かすと次のようなエラーが発生することがあります。
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named NSLayoutConstraint'

stroyboardのプロパティーで"Use Autolayout"のチェックを外すと解消します。

NSLayoutConstraintはiOS 6で追加されものなので、それ以前のバージョンにも対応する必要がある場合は使用できません。

[参考リンク]
http://stackoverflow.com/questions/11252057/nslayoutconstraint-crashes-viewcontroller

2013年2月8日金曜日

safari / iPhone5のホーム画面スクリーンサイズ



iPhone5のsafariの画面をホーム画面におくと、スクリーンサイズが360x480になってしまう場合があります。これはそう遠くないうちに改善されると思いますが、当面の対策がBreaking the Mobile Webに紹介されていました。

ポイントはmetaタグで width=device-width または width=32 が設定されているとスクリーンサイズが360x480に設定されてしまうということで、次のように替えることで回避できます。

<meta name="viewport" content="initial-scale=1.0">
<meta name="viewport" content="width=320.1">

4インチ画面の場合だけこの設定にするためには、2行目について次のJavaScriptで適用します。

if (window.screen.height==568) { // iPhone 4"
    document.querySelector("meta[name=viewport]").content="width=320.1";
}


この他iOS 6のHTML、CSS関連の情報が多く紹介されているのでBreaking the Mobile Webを訪問してみてください。


2013年1月5日土曜日

CoreData error コード



CoreDataのModelを変更すると、往々にして次のようなエラーに出くわします。

persistentStoreCoordinator: Error Error Domain=NSCocoaErrorDomain Code=134100 "The operation couldn’t be completed. (Cocoa error 134100.)"

エラーコードのみ表示され詳細がわからないメッセージですが、エラーコードの定義は"Core Data Constants Reference"にまとめられています。
これまで次のエラーが発生しました。

Cocoa error 1560
NSValidationMultipleErrorsError
Error code to denote an error containing multiple validation errors.

このエラー自体は複数の項目でバリデーションエラーが発生したということですが、私の場合は追加した項目のoptionalにチェックが入っていなかったため必須項目となり、既存レコードでは値がnilであるため発生しました。その他データ型の不一致などでも発生するでしょう。

Cocoa error 134100.
NSPersistentStoreIncompatibleVersionHashError
Error code to denote that entity version hashes in the store are incompatible with the current managed object model.

既存レコードとModelの定義に不整合が生じている場合に発生します。
既存レコードを残しModelの定義を変更する場合は"Add Model Version ..."でModelを追加し、既存レコードをコンバートする必要があります。
Core Data 自動マイグレーションを参照してください。

2012年9月22日土曜日

iOS 6 UINavigationControllerのAutorotate



iOS6ではshouldAutorotateToInterfaceOrientation:メソッドが廃止され、代わりにsupportedInterfaceOrientationsメソッドとshouldAutorotateメソッドを使うようになっています。
ところが最初のViewControllerをUINavigationControllerにしているアプリでは、他のViewControllerにこれらのメソッドを実装しても有効になりません。(shouldAutorotateが呼ばれない。)
このようなアプリの場合は次のようにすると回転を制御できるようになります。
  • UINavigationControllerのサブクラスを作り、起動時のUINavigationControllerのクラスをそれに替える。
  • 上記サブクラスにsupportedInterfaceOrientationsメソッドとshouldAutorotateメソッドを実装する。
  • 一部の画面だけ縦・横の回転を行えうようにする場合、このサブクラスのsupportedInterfaceOrientationsが返す値を画面に応じて変更する。
実装例:
@interface MyNavigationController : UINavigationController
@end

@implementation MyNavigationController

- (NSUInteger)supportedInterfaceOrientations
{
    if ([[self.viewControllers lastObject] isKindOfClass:[RotatableViewController class]]) {
        return UIInterfaceOrientationMaskAllButUpsideDown; 
    } else {
        return UIInterfaceOrientationMaskPortrait;
    }
}

- (BOOL)shouldAutorotate
{
    return YES;
}
@end

shouldAutorotateToInterfaceOrientation:はiOS 6では呼ばれないため、縦横位置に応じたレイアウト調整などをこのメソッドで行っていた場合はこのメソッドの代わりにwillRotateToInterfaceOrientation:duration:メソッドを利用することができます。このメソッドはiOS 5でも呼ばれるため、iOS 6のときだけ必要な処理を行うようにします。また、このようにすればiOS5、iOS6両方に対応可能になります。

実装例:
@implementation RotatableViewController
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    if ([self respondsToSelector:@selector(shouldAutorotate)]) {
        [self shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
    }
}
...
@end

If you want this article in English, let me know.

補足

View Programming Guide for iOSに次のように書かれています。

Dynamically Controlling Whether Rotation Occurs
If you want to temporarily disable automatic rotation, avoid manipulating the orientation masks to do this. Instead, override the shouldAutorotate method on the topmost view controller. This method is called before performing any autorotation. If it returns NO, then the rotation is suppressed.

上記の例の場合は初期のUINavigationControllerが"topmost view controller"になっていると考えられます。この方法でうまくいかない場合は別のWindowが"topmost view controller"になっているというこになります。おそらくはroot view controllerと思われるので、次のメソッドで取得できるUINavigationControllerに対して、上記の方法を適用してみてください。
[myNavContoller.viewControllers.objectAtIndex:0]

ModalViewの場合はそれをコントロールするUINavigationControllerとなります。



2012年8月7日火曜日

3D Translate実装例(2) Diceについて



3D 四面、八面ではY軸中心の回転だけでしたが、DiceではX、Yの2軸で回転させ、さらに落下を出すためにZ軸方向に移動しています。
加えてTransform後のどの面が正面を向いているかを判断しています。

HTMLでのDIVの配置、CSS設定は3D 四面の応用です。

5つの立方体のDiceを配置する。
<div id="view">
  <div id="cube1" onclick="selCube(this, false);">
    <div class="face f1">1</div>
    <div class="face f2">2</div>
    <div class="face f3">3</div>
    <div class="face f4">4</div>
    <div class="face f5">5</div>
    <div class="face f6">6</div>
  </div>
  <div id="cube2" onclick="selCube(this, false);">
    <div class="face f1">1</div>
    <div class="face f2">2</div>
    <div class="face f3">3</div>
    <div class="face f4">4</div>
    <div class="face f5">5</div>
    <div class="face f6">6</div>
  </div>
以下同様にcube3~cube5を配置する。
</div>

(selCubeはtapされたときにそのDiceを選択状態にするfunction)

上記DIVを配置するCSS
全体の位置
#view {
  width:640px;
  height:600px;
  margin:0px auto 0px auto;
  -webkit-transform:translateX(1px);
  -webkit-perspective: 1600;
  -webkit-perspective-origin-x: 550px;
  -webkit-perspective-origin-y: 600px;
}

各cube共通設定
#cube1, #cube2, #cube3, #cube4, #cube5 {
  position:absolute;
  display:none;
  height:200px;
  width:200px;
  -webkit-transform-style: preserve-3d;
  -webkit-transform:translateZ(-3000px);
}

上列3個のDiceのtop位置
#cube1, #cube2, #cube3 {
  margin-top: -240px;
}

下列3個のDiceのY位置
#cube4, #cube5 {
  margin-top: 60px;
}
各Diceの横位置。cube2は中央なので設定不要。
#cube1 { margin-left:320px; }
#cube3 { margin-left:-320px; }
#cube4 { margin-left:160px; }
#cube5 { margin-left:-160px; }

Dice各面の共通設定
.face {
  position: absolute;
  height: 160px;
  width: 160px;
  padding: 20px;
  font-size: 150px;
  text-align;center;
  vertical-align;center;
  color: transparent;
  border:solid 4px darkgray;
  background-color: rgba(50, 50, 50, 0.5);
  -webkit-border-radius: 40px;
}

6面各々の位置決め、およびイメージ設定。
.f1 {
  -webkit-transform: rotateX(90deg) translateZ(100px);
  background-image:url("../Images/1.png");
}

.f2 {
  -webkit-transform: translateZ(100px);
  background-image:url("../Images/2.png");
}

.f3 {
  -webkit-transform: rotateY(90deg) translateZ(100px);
  background-image:url("../Images/3.png");
}

.f4 {
  -webkit-transform: rotateX(180deg) translateZ(100px);
  background-image:url("../Images/4.png");
}

.f5 {
  -webkit-transform: rotateY(-90deg) translateZ(100px);
  background-image:url("../Images/5.png");
}

.f6 {
  -webkit-transform: rotateX(-90deg) translateZ(100px) ;
  background-image:url("../Images/6.png");
}

JavaScript


//Diceコンストラクタ
Dice = function(cube) {
  this.cube = cube;
  this.x = Dice.rand() * 360;
  this.y = Dice.rand() * 360;
  this.z = Dice.rand() * 360;

  this.dx = this.dy = this.dz = 0;
  this.dist = -200;  //traslateZ
  this.sec = 0.1;
  this.interval = 100; //msec;
  this.timer = null;
  //同一DiceでTimerイベントを複数実行しないようにするためのフラグ。
  this.busy = false;
  //trueの間、落下・回転を続ける。
  this.flag = true;
  //何故かiOSでは2,3が入れ替わるので、OSに合わせてfacesのindexを設定
  this.fArr = isIOS ? new Array(1,3,2,4,5,6) : new Array(1,2,3,4,5,6);
}

//Dice初期化. 5個のDiceに対応するインスタンスを作り、cubes配列にセット.
//選択されていないDiceは0とする.
//spotsArrは正面を向いている面が何かを判断するための配列。
Dice.init = function() {
  Dice.spotsArr = new Array(
    (cubes[0].getAttribute("selected") != "true" ? Dice.spotsArr[0] : 0),
    (cubes[1].getAttribute("selected") != "true" ? Dice.spotsArr[1] : 0),
    (cubes[2].getAttribute("selected") != "true" ? Dice.spotsArr[2] : 0),
    (cubes[3].getAttribute("selected") != "true" ? Dice.spotsArr[3] : 0),
    (cubes[4].getAttribute("selected") != "true" ? Dice.spotsArr[4] : 0));
  Dice._seed = Math.random(new Date().getTime());
}

//選択状態を表すために背景色を変更するとBackground-Imageが無効になるので再設定する。
Dice.prototype.setImageUrl = function() {
  this.cube.setAttribute("needsImage", false);
  for(var i=0; i<this.cube.childNodes.length; i++) {
    var face = this.cube.childNodes[i];
    if (face.nodeType == 1) {
      face.style.backgroundImage = face.getAttribute("url");
      face.style.color = "transparent";
    }
  }
}

//乱数生成
Dice.rand = function() {
  Dice._seed = Math.random(Dice._seed);
  return Math.random(new Date().getTime());
}

//タイマーイベントで呼び出される。
Dice.roll = function(o) {
  //selected属性をリセットする。
  o.cube.setAttribute("selected", "false");
  //引数のDiceインスタンスのrollを呼ぶ。
  o.roll();
}

//Diceを振る。WebKitの場合のみ実行。
Dice.prototype.roll = function() {
  if (!isWebKit || this.busy) return;
  this.busy = true;
  //画像がセットされていないときに画像をセット。
  if (this.cube.getAttribute("needsImage") == "true") this.setImageUrl();
  //this.cube.setAttribute("selected", "false");
  try {
    //視点の高さ(距離)設定
    var dist = (isIOS ? -1560 : -800);
    //高さによりロジックを変える。
    if (this.flag && this.dist > dist) {
      //まだdistまで落下していない場合。
      //乱数初期化
      Dice._seed = Math.random(new Date().getTime());
      //乱数を用いて回転角設定。
      var r1 = Dice.rand() * 40;
      var r2 = Dice.rand() * 40;
      var r3 = Dice.rand() * 40;
      //直前の回転角に加え、今回の回転をセット。
      this.x += r1;
      this.y += r2;
      this.z += r3;
      //落下速度調整
      this.dist -= 70;
      //次回の回転をTimerでセット。
      this.timer = setTimeout(Dice.roll, this.interval, this);
    } else {
      //distまで落下した一番正面を向いている面を真正面に向ける。
      var mX = this.x % 90; var amX = Math.abs(mX);
      var mY = this.y % 90; var amY = Math.abs(mY);
      var mZ = this.z % 90; var amZ = Math.abs(mZ);
      if (amX > 1 || amY > 1 || amZ > 1) {
        if (this.interval > 10) this.interval -= 1;
        this.x = (amX > 3 ?  this.x - 3 : this.x - mX);
        this.y = (amY > 3 ?  this.y - 3 : this.y - mY);
        this.z = (amZ > 3 ?  this.z - 3 : this.z - mZ);
        this.flag = false;
        this.timer = setTimeout(Dice.roll, this.interval, this);
        this.interval = 10;
      } else {
        //真正面を向いたら組み合わせ結果を表示。
        this.setSpots();
      }
    }
    //上の計算結果でTransformを実行。
    this.cube.style.webkitTransform = "translateZ(" + this.dist + "px) "
                                    + "rotateX("+ this.x + "deg) "
                                    + "rotateY("+ this.y + "deg) "
                                    + "rotateZ("+ this.z + "deg) ";
    this.cube.style.display = "inline";
  } catch(e) {
    alert("Dice.roll: " + e);
  } finally {
    this.busy = false;
  }
}

//正面を向いている目を判断する。
//X,Y,Z軸各々について回転角から面の位置を判断する。
//Timerイベントで実行されるときはthis=window。
Dice.prototype.setSpots = function() {
  //360°回転すると元に戻るので残りの角度で判断。
  //rotateの順に合わせ、Z、Y、Xの順で判断。順序は重要。
  var rz = this.z % 360;
  //90°毎に面をシフト。
  while(rz >= 90) {
    rz -= 90;
    var t = this.fArr[0];
    this.fArr[0] = this.fArr[4];
    this.fArr[4] = this.fArr[5];
    this.fArr[5] = this.fArr[1];
    this.fArr[1] = t;
  }
  var ry = this.y % 360;
  while(ry >= 90) {
    ry -= 90;
    var t = this.fArr[2];
    this.fArr[2] = this.fArr[4];
    this.fArr[4] = this.fArr[3];
    this.fArr[3] = this.fArr[1];
    this.fArr[1] = t;
  }
  var rx = this.x % 360;
  while(rx >= 90) {
    rx -= 90;
    var t = this.fArr[2];
    this.fArr[2] = this.fArr[5];
    this.fArr[5] = this.fArr[3];
    this.fArr[3] = this.fArr[0];
    this.fArr[0] = t;
  }
  //5つのcubeのどれかを判断。
  var n = new Number(this.cube.id.charAt(4));
  Dice.spotsArr[n-1] = this.fArr[2];
  //5つのDice全部の目がセットされたら役を表示。
  if (Dice.spotsArr[0]>0
   && Dice.spotsArr[1]>0
   && Dice.spotsArr[2]>0
   && Dice.spotsArr[3]>0
   && Dice.spotsArr[4]>0) {
    Dice.showSpots();
  }
}

//役判断用フラグ
Dice.spotsArr = new Array(0,0,0,0,0);
Dice.pair1 = null;
Dice.pair2 = null;
Dice.three = null;
Dice.four = null;
Dice.five = null;
Dice.fullHouse = null;

//回転終了時の正面の目の組み合わせから役を判断、表示する。
Dice.showSpots = function() {
  //役のフラグをクリア
  Dice.pair1 = Dice.pair2 = Dice.three = Dice.four = Dice.five = Dice.fullHouse = null;
  //正面を向いている目の配列をコピーし、ソートする。
  var arr = new Array(Dice.spotsArr[0],Dice.spotsArr[1],Dice.spotsArr[2],Dice.spotsArr[3],Dice.spotsArr[4]);
  arr.sort();
  //役の判断。
  Dice.findCombinations(arr);
  //役のフラグから役の名前を設定。
  var str = "";
  if (Dice.pair1 != null) {
    if (Dice.pair2 == null) str += "One Pair[" + Dice.pair1 +"]<br>";
    else str += "Tow Pairs[" + Dice.pair1 + "," + Dice.pair2 +"]<br>";
  } else if (Dice.three != null) {
     str += "Three Of A Kind[" + Dice.three +"]<br>";
  } else if (Dice.four != null) {
     str += "Four Of A Kind[" + Dice.four +"]<br>";
  } else if (Dice.five != null) {
     str += "Five[" + Dice.five +"]<br>";
  } else if (Dice.fullHouse != null) {
     str += "Full House[" + Dice.fullHouse +"]<br>";
  } else {
     str += "All Different[" + arr +"]<br>";
  }
  //役を表示。
  var div = document.getElementById("memo");
  div.innerHTML = str + "\n" + Dice.spotsArr;
}

//役を見つける。
Dice.findCombinations = function(arr) {
  var i=1, j=0, n=0, s=arr[0];
  for(; i<5; i++) {
    if (s == arr[i]) {
      n++;
    } else {
      Dice.setPairs(n+1, s);
      n = 0;
      s = arr[i];
    }
  }
  if (n > 0) Dice.setPairs(n+1, s);
  if (Dice.three && Dice.pair1) {
    Dice.fullHouse = Dice.three + "," + Dice.pair1;
    Dice.three = Dice.pair1 = null;
  }
}

//pair, three, four, fiveのフラグをセット。
Dice.setPairs = function(n, s) {
  if (n == 2) {
    if (Dice.pair1 == null) Dice.pair1 = s;
    else Dice.pair2 = s;
  } else if (n == 3) {
    Dice.three = s;
  } else if (n == 4) {
    Dice.four = s;
  } else if (n == 5) {
    Dice.five = s;
  }
}

//----- Diceを使用するfunction -----

var cubes = null;

//初期化(body.onloadなどで実行)
function init() {
  //cubeのDIVを配列にセット
  cubes = new Array(cube1, cube2, cube3, cube4, cube5);
  var divs = document.getElementsByTagName("DIV");
  //全Diceを選択状態にする。
  selAll();
  //Diceを振る。
  roll();
}

//選択状態のDiceを振る。
function roll() {
  Dice.init();
  for(var i=0; i<5; i++) {
    if (cubes[i].getAttribute("selected") == "true") new Dice(cubes[i]).roll();
  }
}

//Diceを選択状態にセット、または反転する。
function selCube(c, selAll) {
  if (!selAll && c.getAttribute("selected") == "true") {
//touchまたはclickされ、選択状態の場合は非選択状態にする。
    c.setAttribute("selected", "false");
    new Dice(c).setImageUrl();
  } else {
//sellAllから呼ばれた時、または非選択状態のときは選択状態にする。
    c.setAttribute("needsImage", "true");
    c.setAttribute("selected", true);
    var state = "true";
    //cの子要素の背景色、文字色を設定。
    var color = "rgba(130, 130, 255, 0.5)"; //半透明
    for(var i=0; i<c.childNodes.length; i++) {
      if (c.childNodes[i].nodeType == 1) { //ElementType
        if (state == "true") c.childNodes[i].style.background = color;
        if (state == "true") c.childNodes[i].style.color = "black";
      }
    }
  }
}

function selAll() {
  for(var i=0; i<5; i++) {
    selCube(cubes[i], true);
  }
}

3D Translate実装例(2) Diceについて

2012年7月25日水曜日

3D Translateの実装例



Nack Labで紹介している"ダイス"、およびTech Sampleの"3D 四面"、"3D 八面"はcss trahnsformを使用して3Dの動きを実現しています。

主にPaul R. Hayes氏のExper­i­ment: 3D cube with touch ges­tures and click and dragを参考にしています。

ここでは直方体を横方に回転させる"3D 四面"を例に取り上げて説明します。

4平面を作るためにDIV

<div id="divFaces">
  <div id="cube">
    <div class="face one"   id="one">One</div>
    <div class="face two"   id="two">Two</div>
    <div class="face three" id="three">Three</div>
    <div class="face four"  id="four">Four</div>
  </div>
</div>

上記DIV対応するCSS

全体を包むdivFacesの設定

#divFaces
{
  -webkit-perspective: 420;
  -webkit-perspective-origin: 50% 100px;
}

-webkit-perspective:
視点からの距離。数値が大きいほど望遠、小さいほど広角な効果となります。

-webkit-perspective-origin:
座標軸の中心の位置。この場合、横方向中央(50%)、縦は上から100pxとなります。

divFaces内の直方体の設定

#cube {
  -webkit-transition: -webkit-transform 0.2s linear;
  -webkit-transform-style: preserve-3d;
  margin: -150px auto 0px auto;
  height: 150px;
  width: 200px;
}

-webkit-transform-style: preserve-3d;
3D座標変換を行うための設定。

-webkit-transition: -webkit-transform 0.2s linear;
アニメーションの設定。3D座標変換とは関係ない。
margin、height、widthは適宜設定する。

四面に共通の設定

.face {
  position:absolute;
  height: 100%;
  width: 100%
  padding:0px;
  background-color: rgba(50, 50, 50, 0.5);
  font-size: 27px;
  line-height: 1em;
  color: #fff;
  border: 1px solid #555;
}

position:absolute;

同一平面上に配置するため。これがないと四面のDIVが縦方向に重ならずに並ぶ。

height: 100%;
width: 100%;

cubeのサイズと一致するように100%をセット。100%である必要はなく、pxなどの設定でもよい。

各面についての設定

#cube .one {
  -webkit-transform: translateZ(100px);
}
第1面はZ軸方向に+100px移動。手前に近付いている。

#cube .two {
  -webkit-transform: rotateY(90deg) translateZ(100px);
}
第2面はY軸で+90度回転(まわれ右)してから、Z軸で100px移動(右へ移動)。

#cube .three {
    -webkit-transform: rotateY(180deg) translateZ(100px);
}
第3面はY軸で+180度回転(後向き)してから、Z軸で100px移動(遠ざかる)。

#cube .four {
    -webkit-transform: rotateY(-90deg) translateZ(100px);
}
第4面はY軸で-90度回転(まれれ左)してから、Z軸で100px移動(左へ移動)。

直方体回転

次のようなループで直方体を連続回転させることができます。

//touch, mouseの移動量を yAngle にセットします。
var yAngle = 0;

//TransformでrotateYを行い、Y軸を中心に回転させます。
function rotate() {
  yAngle += 10;
  cube.style.webkitTransform = "rotateY(" + yAngle + "deg)";
  setTimeout("rotate()", 20);
}

yAngleは初期状態からの回転角で、直前に行ったTransformからの差分ではありません。
ループで回転させる場合はsetTimeoutを使用します。

3D Transformの留意点

3Dのtransformで頭が混乱するのは主に次の二点でしょう。
  • XYZについてのtransformの順序で結果が異なる。
    上の例ではY軸で回転してからZ軸で移動しているが、Z軸で移動してからY軸で回転したのでは全く異なる結果となります。
  • rotateは座標の回転で、オブジェクトの回転ではない。
    上の例ではY軸で回転してていますが、Z軸も共に回転しているため、回転後の移動は前後の移動とは限らず、回転角に応じた方向に移動することになります。
3D 四面、八面のソースコード

//コンストラクタ
Faces = function(divFaces, divCube) {
  //直方体が配置されていDIV(divFaces)
  this.div = divFaces;
  this.cubeDiv = divCube;
  //移動量計算のため直前のtouch位置をセット。初期値=Number.MIN_VALUE
  this.x = Number.MIN_VALUE;
  //Y軸を中心とした回転角。
  this.y = 0;
  //Startイベントハンドラをセット。
  this.div.addEventListener(Faces.EventStart, Faces.touchStart, false);
  Faces.instance = this;
  Faces.div = this.div;
}

//iOS, デスクトップでイベント名を使い分ける。
Faces.EventStart = isIOS ? "touchstart" : "mousedown";
Faces.EventMove = isIOS ? "touchmove" : "mousemove";
Faces.EventEnd = isIOS ? "touchend" : "mouseup";
Faces.EventOut = isIOS ? null : "mouseout";

//Facesが作られたときの引数のdiv。イベントが発生するDIVと異なる。
Faces.div = null;

//Startイベントで作られたオブジェクト。
Faces.instance = null;

//Startイベントハンドラ。Moveイベントハンドラセット。
//thisはイベントが発生するDIV.
Faces.touchStart = function() {
  if (Faces.div == null) return;
  Faces.div.addEventListener(Faces.EventMove, Faces.touchMove, false);
  Faces.div.addEventListener(Faces.EventEnd, Faces.touchEnd, false);
  if (!isIOS) {
    Faces.div.addEventListener(Faces.EventOut, Faces.touchEnd, false);
  }
}

//Moveイベントハンドラ。
Faces.touchMove = function() {
  if (Faces.instance == null) return;
  Faces.instance.newX = event.pageX;
  with (Faces.instance) {
   if (x != Number.MIN_VALUE) {
      y += (newX - x);
      var m = y % 90;
      if (m <= 2 &&  m >= -2) {
        y -= m;
      }
      cubeDiv.style.webkitTransform = "rotateY(" + y + "deg)";
    }
    x = newX;
  }
}

//Endイベントハンドラ。End, Moveイベントハンドラ解除。
Faces.touchEnd = function() {
  if (Faces.instance == null) return;
  Faces.instance.x = Number.MIN_VALUE;
  var div = Faces.instance.div;
  div.removeEventListener(Faces.EventMove, Faces.touchMove);
  div.removeEventListener(Faces.EventEnd, Faces.touchEnd);
  div.removeEventListener(Faces.EventOut, Faces.touchEnd);
}

Faces使用例

上記のようにDIVを配置し、body.onloadなどでFacesを作る。

<body onload="new Faces(divFaces, cube)">

3D 八面の場合

八面の場合もJavaScriptは共通で、面のモデリング、CSSが異なります。

<div id="divFaces8" style="margin-top:180px">
    <div id="cube8">
      <div class="face8 f1" id="f1">One</div>
      <div class="face8 f2" id="f2">Two</div>
      <div class="face8 f3" id="f3">Three</div>
      <div class="face8 f4" id="f4">Four</div>
      <div class="face8 f5" id="f5">Five</div>
      <div class="face8 f6" id="f6">Six</div>
      <div class="face8 f7" id="f7">Seven</div>
      <div class="face8 f8" id="f8">Eight</div>
  </div>
</div>

Facesを作成するときの引数を8面のものに変更します。

<body onload="new Faces(divFaces8, cube8)">

8面の場合のCSS

#divFaces8
{
  -webkit-perspective: 250;
  -webkit-perspective-origin: 50% 80px;
}

#cube8 {
  position:relative;
  margin: -100px auto 0px auto;
  height: 133px;
  width: 100px;
  -webkit-transition: -webkit-transform 0.2s linear;
  -webkit-transform-style: preserve-3d;
}

.face8 {
  position:absolute;
  height: 120px;
  width: 100px;
  padding: 0px;
  background-color: rgba(50, 50, 50, 0.5);
  font-size: 20px;
  line-height: 1em;
  color: #fff;
  border: none;
}

#cube8 .f1 {
    -webkit-transform: translateZ(120px);
    background:gray;
}

#cube8 .f2 {
    -webkit-transform: rotateY(45deg) translateZ(120px);
    background:green;
}

#cube8 .f3 {
    -webkit-transform: rotateY(90deg) translateZ(120px);
    ---webkit-transform: translateZ(-100px);
    background:blue;
}

#cube8 .f4 {
    -webkit-transform: rotateY(135deg) translateZ(120px);
    background:yellow;
}

#cube8 .f5 {
    -webkit-transform: rotateY(180deg) translateZ(120px);
    background:red;
}

#cube8 .f6 {
    -webkit-transform: rotateY(225deg) translateZ(120px);
    background:green;
}

#cube8 .f7 {
    -webkit-transform: rotateY(270deg) translateZ(120px);
    background:blue;
}

#cube8 .f8 {
    -webkit-transform: rotateY(315deg) translateZ(120px);
    background:yellow;
}

Manifest+LocalStorageによるオフラインアプリ作成

2012年7月24日火曜日

Cache Manifestを無効にする方法



以下の記述はSafari/iOSで試した結果のもので、Safari/デスクトップでは若干異なる場合があるようです。また他のブラウザではチェックしていません。

いったん有効にしたCache Manifestを無効にする場合、単純にhtmlタグから宣言を削除しただけではうまくいきません。

既にキャッシュが有効になっている場合、htmlタグからmanifest宣言を削除してもその変更はブラウザに伝わりません。manifestファイルも更新する必要があります。
こうすると一見manifestは使われないように見えますが、ブラウザはその後もmanifestファイルをチェックしつづけ、サーバーにmanifestファイルが残っていると変更なしとなり、キャッシュを使い続けます。
こうなってからサーバーのmanifestファイルを削除しても実は手遅れで、Net不通状態と判断されるためか、キャッシュが使われ続けます。
こうして、リソースの変更が反映しないWebページになってしまうことがあります。
この動作はバグとも言えそうなので、今後変更される可能性があるでしょう。

次の手順で行うと、manifestファイルを参照しなくなります。

  1. サーバーからmanifestファイルを削除またはりネーム
  2. ブラウザからアクセス(いったんFile Not Foundの状態を作る)
  3. htmlタグからmanifest宣言を削除

開発環境では上記手順を実行することが可能ですが、実稼働環境ではブラウザからのアクセスを確認することが困難です。次善の策として、次のような手順が考えられます。


  1. htmlタグのmanifest宣言のファイル名を存在しないファイル名に変更し、manifestファイルを更新する。
    <html manifest="a.manifest">を<html manifest="-.manifest">のように変更します。
    拡張子はMIME TYPEに登録されているものにしておきます。さもないと、再度manifestを有効にしたいときに苦労します。Safari/iOSではキャッシュデータの削除を行わないと回復できませんでした。
  2. このまましばらく運用し、対象ブラウザが全て一度は"-.manifest"にアクセスしたと判断できたら、htmlタグからmanifest宣言を削除、manifestファイルを更新する。

manifest="-"へのアクセスの負荷は軽いので、1のままでも実害はほとんどないでしょう。再度Cacheを有効にしたい場合はmanifest宣言のファイル名を元に戻します。

Safari/iOSからIISにアクセスしたときのログ

設定
a.manifestファイルでindex.htmlとXmlReq.jsをキャッシュ。
NETWORK:*を設定し、XMLHttpRequestは常にNETWORKから取得。
acuse指示XMLHttpRequestでdata.txtにアクセス。

Cacheが有効な場合
manifestファイルのstatush304(Not Modified)
01:57:45 172.30.10.11 GET /a/a.manifest 304
01:57:45 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlのhtmlタグからmanifest宣言を削除
manifestが更新されていないのでキャッシュが使用される。
01:58:42 172.30.10.11 GET /a/a.manifest 304
01:58:42 172.30.10.11 GET /a/Resources/data.txt 200

a.manifest更新
キャッシュが更新される。
01:59:30 172.30.10.11 GET /a/a.manifest 200
01:59:30 172.30.10.11 GET /a/Resources/data.txt 200
01:59:30 172.30.10.11 GET /a/index.html 200
01:59:30 172.30.10.11 GET /a/Resources/XmlReq.js 304
01:59:30 172.30.10.11 GET /a/index.html 200

再度アクセス
indexから宣言が削除されているがmanifest参照は有効なまま。index.html、XmlReq.jsはキャッシュが使用される。
01:59:37 172.30.10.11 GET /a/a.manifest 304
01:59:37 172.30.10.11 GET /a/Resources/data.txt 200

XMLHttpRequestでdata.txtにアクセス
既にmanifestチェック済みなのでdata.txtのみアクセスされる。
02:44:27 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlのmanifest宣言復活、a.manifest更新
02:49:18 172.30.10.11 GET /a/a.manifest 200
02:49:18 172.30.10.11 GET /a/Resources/data.txt 200
02:49:18 172.30.10.11 GET /a/index.html 200
02:49:18 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:49:18 172.30.10.11 GET /a/index.html 200

a.manifest削除
manifestファイルのstatusは404(Not Found)となる。
この時点ではindex.html、XmlReq.jsはキャッシュが使用される。
02:50:32 172.30.10.11 GET /a/a.manifest 404
02:50:32 172.30.10.11 GET /a/Resources/data.txt 200

index.htmlからmanifest宣言を削除
manifest参照は行われず、index.html、XmlReq.jsへのアクセス発生。
02:53:37 172.30.10.11 GET /a/index.html 200
02:53:37 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:53:39 172.30.10.11 GET /a/Resources/data.txt 200

manifestが有効な状態で、index.htmlのmanifest宣言のファイル名を存在しないもの("-.manifest")に変更、manifestファイル更新
キャッシュが更新される。
02:58:48 172.30.10.11 GET /a/a.manifest 200
02:58:48 172.30.10.11 GET /a/Resources/data.txt 200
02:58:48 172.30.10.11 GET /a/index.html 200
02:58:48 172.30.10.11 GET /a/Resources/XmlReq.js 304
02:58:48 172.30.10.11 GET /a/index.html 200

再度アクセス
"-.manifest"は存在しないのでstatusは404となる。一度もmanifestファイルが読まれていない状態のため、キャッシュは無効になり、index.html、XmlReq.jsへのアクセスが発生する。
03:00:41 172.30.10.11 GET /a/index.html 304
03:00:41 172.30.10.11 GET /a/- 404
03:00:41 172.30.10.11 GET /a/Resources/XmlReq.js 304
03:00:41 172.30.10.11 GET /a/Resources/data.txt 304

index.htmlのmanifest宣言を削除
manifest参照は行われず、index.html、XmlReq.jsへのアクセス発生。
03:03:24 172.30.10.11 GET /a/index.html 200
03:03:24 172.30.10.11 GET /a/Resources/XmlReq.js 304
03:03:24 172.30.10.11 GET /a/Resources/data.txt 200

Cache Manifestを無効にする方法

2012年7月22日日曜日

Cache Manifest使用時のXMLHttpRequest



Cache Manifestを使用したオフラインアプリでもXMLHttpRequestを使用することができます。次のような点に注意すれば、XMLHttpRequestを利用してCacheを残しつつ、オンライン時に一部のデータだけ更新することができます。
以下の方法はSafari/iOSでのみチェックしています。

1.Cache Manifestの設定
次の設定をCache Manifestに含める。
NETWORK:
*
これを設定しないと、Cacheが有効なときはXMLHttpRequestのレスポンスが空となります。

2.XMLHttpRequeststatusのstatus=0チェック
Cacheが有効なときはXMLHttpRequeststatusのstatusコードが0となり、レスポンスが空になります。
ただし、XMLHttpRequeststatusのQuery StringがついたURLの場合、ホーム画面のアイコンからアクセスする場合はstatus=0ですが、Safariのアドレスバーからアクセスするとstatus=200となり、レスポンスはCacheされているデータとなります。

3.localStroageとの併用がお勧め
status=0のときにネットワークから取得したデータを使うことができるようにするため、XMLHttpRequeststatusで取得したデータはlocalStroageに保存しておくのがお勧めです。

XMLHttpRequest実装例

XmlReq = function(url) {
  this.url = url;
}

//elm = responseのcontentをセットする要素。
XmlReq.prototype.send = function(elm) {
  var req = this.createHttpRequest();
  req.open("GET", this.url, true); //true = 非同期。
  req.onreadystatechange =
    function() { //受信時に起動するイベントハンドラ
      if (req.readyState == 4) { //4 = 受信完了 (サーバ処理終了)
        var content = null;
        if (req.status == 200) {
          content = req.responseText;
          localStorage["content"] = content;
        } if (req.status == 0) {
          content = localStorage["content"];
        }
        if (content != null) {
          if (elm.tagName == "INPUT") {
            elm.value = content;
          } else if (elm.tagName == "DIV") {
            elm.innerText = content;
          } else {
            //適宜追加
          }
        }
      }
    };
  req.send(null);
}

XmlReq.prototype.createHttpRequest = function() {
  if(window.ActiveXObject){ //Win IE
    try {
      return new ActiveXObject("Msxml2.XMLHTTP");
    } catch(e) { //MSXML2以前
      return null;
    }
  } else if(window.XMLHttpRequest){
    //Win ie以外のXMLHttpRequestオブジェクト実装ブラウザ用
    return new XMLHttpRequest();
  } else {
    logDiv.innerHTML += "<br><null>";
    return null;
  }
}

XmlReq.prototype.toString = function() {
  return this.url;
}

XmlReqの利用例

function sendRequest() {
  var ID = new Date().getTime();
  var req = new XmlReq("http://" + location.host + "/SampleApp/Resources/data.txt?ID=" + ID);
  req.send(hiddenField);
}
function showData() {
  alert(hiddenField.value);
}

<input type="hidden" id="hiddenField">

2012年7月11日水曜日

localStorageの実装例



NackLabに載せているHTMLアプリは、ほとんどがlocalStorageを利用しています。その中の一例を紹介します。

localStorageのメソッドの拡張例(エラー処理などを簡略化してあります)
localStorageにptorotypeでメソッドを追加することができないため、LocalStorageをつくり、Stringの値と、それをNumber、boolに変換して返すfunctionを追加しています。

//コンストラクタ。この例ではLocalStorageを宣言しているだけ。
LocalStorage = function() { }

//key、valueのセット。
LocalStorage.set = function(key, val) {
  localStorage[key] = val;
}

//LocalStorageからデータを読み出す。Stringが返される。
LocalStorage.stringValue = function(key, defaultValue) {
  var str = localStorage[key];
  if (str == null) return defaultValue;
  return str;
}

//keyの値を数値として返す。値が設定されていない場合は、数値化でエラーとなった場合はdefaultValueを返す。
LocalStorage.numberValue = function(key, defaultValue) {
  var val = null;
  try {
    val = localStorage[key];
    if (val == null) val = defaultValue;
    return new Number(val);
  } catch(e) {
    return defaultValue;
  }
}

//keyの値をboolとして返す。値が設定されていない場合はdefaultValueを返す。
LocalStorage.boolValue = function(key, defaultValue) {
  var val = null;
  val = localStorage[key];
  if (val == null) val = defaultValue;
  return (val.toLowerCase() == "true");
}

LocalStorageの呼び出し例

//body.onloadで呼ぶ
function init() {
  divCount.innerText = LocalStorage.numberValue("count", 5);
  cbTraining.checked = LocalStorage.boolValue("training", false);
}

//onunloadで呼ぶ。
function save() {
  LocalStorage.set("count", divCount.innerText);
  LocalStorage.set("training", cbTraining.checked);
}

localStorageの実装例

2012年7月3日火曜日

Site.Masterの選択中のタブStyle変更



VisualStudio 2010でWebアプリケーション プロジェクトを追加すると、Site.Masterによ
りタブでページ切り替えを行うテンプレートがコピーされます。
タブクリックでページを切り替えたあと、選択中のページのタブのスタイルを変更したく
なるのですが、なかなか思うようにいきませでした。次のようにすると変更できます。

SiteMaster.csにメソッド追加

protected void Page_Load(object sender, EventArgs e)
{
    NavigationMenu.Load += new EventHandler(NavigationMenu_Laod);
}

protected void NavigationMenu_Laod(object sender, EventArgs e)
{
    string requestUrl = Request.Url.Segments[Request.Url.Segments.Length - 1];
    foreach (MenuItem item in ((Menu)sender).Items)
    {
        if (item.NavigateUrl.EndsWith(requestUrl))
        {
            item.Selected = true;
            break;
        }
    }
}

SiteMaster.aspxにstyle追加

    <style type="text/css">
        div.menu ul li a.selected
        {
        background-color:LightYellow;
        color:Black;
        }
        /*disabledしたタブのstyle*/
        div.menu ul li a.aspNetDisabled
        {
        background-color:#CCCCCC;
        color:#777777;
        }
    </style>