2017年2月18日土曜日

CMMotionManagerの利用(2)



AppStoreに載せているPanViewerは、パノラマ写真をiPhone/iPadを水平方向に回転させて見るアプリです。
このアプリはCMMotionManagerをからGyroデータ取得し、デバイスの回転角度に応じて画像を移動させています。

その方法について紹介ですが、今回は前回のサンプルの次の問題点に対処します。
・画像移動がギクシャクする。
・画像が画面からはみ出すとその部分が空白になる。

前者についてはanimationを利用して画像移動をスムーズにします。
→ func moveImageLayer 参照

後者については同じ画像をセットしたUIImageViewを3つ並べ、空白が生じないようにします。
→ func setImageViewSize 参照

また、移動後の位置が画面からはみ出す場合に位置調整を行います。
→ func newPosition 参照

今回のサンプルアプリのプロジェクトはここからダウンロードできます。

以下、前回との変更点だけ載せます。

    //画像を表示するUIImageView。画像が画面からはみ出した時に画像を繋げて表示するために3つ使う。
    @IBOutlet weak var imageViewL: UIImageView? //Left
    @IBOutlet weak var imageViewC: UIImageView? //Center
    @IBOutlet weak var imageViewR: UIImageView? //Right
  
    //3つのUIImageViewのsuperview。
    //画像移動はimageViewContainerのlayerを用いて行う。
    //位置判定が容易になるよう、frame.originは(0,0)とし、サイズはimageViewCと同じにする。
    @IBOutlet weak var imageViewContainer: UIView?
  
    //中央のUIImageViewの左右の端が画面に入ると画像外の部分が空白になる。
    //その空白を埋めるために左右にもUIImageViewを置き、同じ画像をセットする。
    func setImage(imageName: String) {
        if let image = UIImage(named: imageName) {
            if self.imageViewC!.image != image {
                self.imageViewL!.image = image; //左側
                self.imageViewC!.image = image; //中央
                self.imageViewR!.image = image; //右側
            }
        }
    }
    
    //3つのUIImageViewとimageViewContainerが画面にフィットするようにサイズ、位置を設定する。
    func setImageViewSize(viewSize: CGSize) {
        self.adjX = 0
        //originX: 3つのUIImageViewのorigin.xを設定するために用いる。
        var originX: CGFloat = 0;
        //3つのimageView及びimageViewContainerのサイズを設定する。
        for imageView in [self.imageViewL!, self.imageViewC!, self.imageViewR!] {
            if let image = imageView.image {
                //imageViewが画面にフィットするようにサイズを設定する。
                imageView.frame.size.width = image.size.width * viewSize.height / image.size.height
                imageView.frame.size.height = viewSize.height
                imageView.frame.origin.y = 0;
                //origin.xを-imageView.frame.size.width, 0, imageView.frame.size.widthの順にセット。
                imageView.frame.origin.x = originX - imageView.frame.size.width;
                originX += imageView.frame.size.width;
                //imageViewCに合わせてimageViewContainerのサイズを設定。
                if imageView == self.imageViewC {
                    self.imageViewContainer!.frame.size = self.imageViewC!.frame.size
                    self.imageViewContainer!.layer.position.x = 0
                }
            }
        }
    }
    
    //self.imageViewContainer.layerの位置を変えることで画像を水平方向に移動する。
    //画像移動をスムーズにするためanimationを使用する。
    func moveImageLayer() {
        let (from, to) = self.newPosition()
        let anim: CABasicAnimation = CABasicAnimation(keyPath: "position")
        anim.fromValue = from
        anim.toValue = to
        anim.duration = self.interval
        //animation終了後に初期位置に戻るので、layer.positionを移動後の位置にしておく。
        self.imageViewContainer!.layer.position = to
        self.imageViewContainer!.layer.add(anim, forKey:"move-layer")
    }
    
    //デバイスの回転から画像の表示位置を計算する。
    func newPosition() -> (CGPoint, CGPoint) {
        var from = self.imageViewContainer!.layer.position
        //deviceMotionのquaternionの値から回転角を求め、水平方向の移動量を計算する。
        if let deviceMotion = self.motionManager?.deviceMotion {
            let attitude: CMAttitude = deviceMotion.attitude
            let q: CMQuaternion = attitude.quaternion
            //angleはimageの視野角。360、180など。iPhoneで撮影したパノラマ写真なら180。
            let tx = (CGFloat)(atan2(q.y, q.x) / Double.pi * (360 / angle))
            let w = self.imageViewC!.frame.size.width
            var x = w * tx + adjX;
            //初期状態ではadjX=0。この場合、画像表示位置と計算上の位置が一致するようにadjXを設定する。
            if adjX == 0 {
                adjX = from.x - x
                x = from.x
            }
            let maxX = w / 2
            var posAdjusted = false
            //mageViewCの移動後、画面に欠ける部分ができる場合画面を覆う位置に調整する。
            //atan2の値はpiから-pi、またはその逆へ非連続に変化する。
            //視野角により画像のwidthより大きな変化(180度の場合はwidth*2前後)になるため、whileで適正位置に移動するまでループする。
            if x > maxX {
                //mageViewCが画面の右端から画像が消える位置に移動する場合。
                while(x > maxX) {
                    adjX -= w
                    x -= w
                }
                posAdjusted = true
            } else if x < -maxX {
                //mageViewCが画面の左端から画像が消える位置に移動する場合。
                while(x < -maxX) {
                    adjX += w
                    x += w
                }
                posAdjusted = true
            }
            if posAdjusted {
                //位置調整が行われた場合、layerの移動量がmaxX以上になったら調整後の位置に合わせてlayerのanimation開始位置も調整する。
                //少々いい加減な判断だが、よほど激しく回転させなけばチラつきは発生しない。
                let dx = x - self.imageViewContainer!.layer.position.x
                if dx > maxX {
                    from.x += w;
                } else if dx < -maxX {
                    from.x -= w;
                }
                //位置調整時はanimationを使用しない。
                self.imageViewContainer!.layer.position.x = from.x
            }
            return (from, CGPoint(x:x, y:from.y))
        }
        return (from, from)
    }




2017年2月16日木曜日

CMMotionManagerの利用(1)



AppStoreに載せているPanViewerは、パノラマ写真をiPhone/iPadを水平方向に回転させて見るアプリです。
このアプリはCMMotionManagerをからGyroデータ取得し、デバイスの回転角度に応じて画像を移動させています。その方法について紹介します。

今回はロジックを単純にし、肝心な部分をわかりやすくしています。

ポイントは次の3点です。
・CMMotionManagerからdeviceMotionのデータを取得する。
・attitude.quaternionのx、yの値からatan2関数でデバイスの水平方向の角度を得る。
・画像を角度に応じて水平方向に移動させる。

正直なところ、attitude.quaternionの値とatan2関数を使う方法は試行錯誤の結果たどり着いたものです。このアプリではデバイスを自分の前に構えて体を回転させ、それに応じて画像を動かすことを意図しています。attitude.rollでは体は動かさず手で回転させた場合は近い動作になりますが、体を回転させた場合は期待通りになりません。また、回転軸がデバイスの縦軸のため、横にした場合はattitude.pitchの方が近い動作になり、対応困難になります。

このサンプルでは次の問題があります。
・画像移動がギクシャクする。
・画像が画面からはみ出すとその部分が空白になる。

これらについては次回のサンプルで対処します。

サンプルアプリのプロジェクトはここからダウンロードできます。

以下はプロジェクト中のViewControllerのコード(一部を省略)です。このサンプルでは画像移動に関するコードがあるのはこのクラスだけです。

//CoreMotionをインポート
import UIKit
import CoreMotion

//このサンプルでは表示画面はViewControllerだけ
class ViewController: UIViewController {

    //CMMotionManagerのインスタンスをセットする。
    var motionManager: CMMotionManager?

    //画像を表示するUIImageView。画像の左右の端が画面内にある時に画像を繋げて表示するために3つ使う。
    @IBOutlet weak var imageView: UIImageView? //Center
  
    //3つのUIImageViewのsuperview。
    //画像移動はimageViewContainerのlayerを用いて行う。
    //位置判定が容易になるよう、frame.originは(0,0)とし、サイズはimageViewと同じにする。
    @IBOutlet weak var imageViewContainer: UIView?
    
    //motionManagerの値から計算した位置を画像の画像位置に合わせるための調整値。
    var adjX: CGFloat = 0
    
    //画像視野角。iPhoneで撮影したパノラマ写真の場合は概ね180度。
    let angle = 180.0
    
    //motionManagerから位置情報を取得するタイマーイベント間隔。
    let interval = 0.1
    
    //viewWillAppearで画像設定
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.imageView!.frame = self.view.frame
        self.imageViewContainer!.frame = self.view.frame
        self.setImage(imageName: "ashinoko.jpg");
        self.setImageViewSize(viewSize: self.view.frame.size)
    }
    
    //画面表示後にmoveImageを呼ぶ。moveImageはタイマーイベントでループ実行する。
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        moveImage()
    }
  
    //デバイス回転時に縦、横の画面サイズに合わせてImageViewのサイズを変更する。
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        self.setImageViewSize(viewSize: size)
    }
    
    //motionManagerをスタートさせる。
    func startMotionManger() {
        if self.motionManager == nil {
            self.motionManager = CMMotionManager()
        }
        self.motionManager!.startDeviceMotionUpdates()
    }
    
    //motionManagerをストップさせる。
    func stopMotionManger() {
        NSObject.cancelPreviousPerformRequests(withTarget:self);
        if let motionManager = self.motionManager {
            motionManager.stopDeviceMotionUpdates()
        }
        self.motionManager = nil;
    }

    //imageViewに画像をセットする。
    func setImage(imageName: String) {
        if let image = UIImage(named: imageName) {
            if self.imageView!.image != image {
                self.imageView!.image = image;
            }
        }
    }
    
    //3つのUIImageViewとimageViewContainerが画面にフィットするようにサイズ、位置を設定する。
    func setImageViewSize(viewSize: CGSize) {
        self.adjX = 0
        //3つのimageView及びimageViewContainerのサイズを設定する。
        if let imageView = self.imageView {
            if let image = imageView.image {
                //imageViewが画面にフィットするようにサイズを設定する。
                imageView.frame.size.width = image.size.width * viewSize.height / image.size.height
                imageView.frame.size.height = viewSize.height
                //imageViewに合わせてimageViewContainerのサイズを設定。
                self.imageViewContainer!.frame.size = imageView.frame.size
                self.imageViewContainer!.layer.position.x = 0
            }
        }
    }
    
    //タイマーイベントで実行し、デバイスの回転に応じて画像を水平方向に移動する。
    func moveImage() {
        if self.motionManager != nil {
            self.moveImageLayer()
        } else {
            self.startMotionManger();
        }
        self.perform(#selector(moveImage), with: nil, afterDelay: interval)
    }

    //self.imageViewContainer.layerの位置を変えることで画像を水平方向に移動する。
    //animationなし。ロジックはシンプル。
    func moveImageLayer() {
        let to = self.newPosition()
        self.imageViewContainer!.layer.position = to
    }

    func newPosition() -> CGPoint {
        let from = self.imageViewContainer!.layer.position
        //deviceMotionのquaternionの値から回転角を求め、水平方向の移動量を計算する。
        if let deviceMotion = self.motionManager?.deviceMotion {
            let attitude: CMAttitude = deviceMotion.attitude
            let q: CMQuaternion = attitude.quaternion
            //angleはimageの視野角。360、180など。iPhoneで撮影したパノラマ写真なら180。
            let tx = (CGFloat)(atan2(q.y, q.x) / Double.pi * (360 / angle))
            let w = self.imageView!.frame.size.width
            var x = w * tx + adjX;
            //初期状態ではadjX=0。この場合、画像表示位置と計算上の位置が一致するようにadjXを設定する。
            if adjX == 0 {
                adjX = from.x - x
                x = from.x
            }
            return CGPoint(x:x, y:from.y)
        }
        return from
    }
}

2017年1月20日金曜日

[__NSArrayI pointValue]: unrecognized selector sent to instance ...


swiftでコーディング中に次のエラーが発生した
[__NSArrayI pointValue]: unrecognized selector sent to instance

NSArrayにセットしたオブジェクトがCGPointと想定されるが、それ以外の値がセットされている場合に発生する。

Googleで検索するとintの場合なども出てくる。
[__NSArrayM intValue]: unrecognized selector sent to instance

私の場合、Appleのドキュメントにある次のサンプルに倣ってCABasicAnimationに数値のArrayをセットしたところ発生した。
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = [0, 0]
animation.toValue = [100, 100]

単独のアニメーションはこれで動作するが、ループで繰り返し実行中にデバイスを回転させると上記エラーが発生する。

次のように変更するとエラーが発生しなくなる。
anim.fromValue = CGPoint(x:0, y:0)
anim.toValue = CGPoint(x:100, y:100)

CABasicAnimation のfromValue、toValueはkeyPathによって異なる値をとるので、なんでもセットすることができてしまう。
"position"の場合は基本的にはCGPointとswiftのArrayの両方に対応しているが、どこかに未対応のところが残っているため、発生するのだろう。

このようなエラーが発生した場合はObjective-Cのドキュメントを参照し、その場合と型が異なっていないかチェックしてみるとよいでしょう。

2016年9月9日金曜日

Launch Screen.storyboardの画像が表示されない

起動画面にLaunch Screen.storyboardを使ったアプリを作成中、storyboardで設定した画像が真っ黒になるという現象が発生した。

Simulatorでは表示されるが、TestFrightでインストールしたバージョンで発生した。

一旦別の画像を設定・ビルドし、再度画像を設定し直すと、とりあえずはまた画像が表示されるようになったが、再発するので調べてみたら、同様のことが載っているページがいくつか見つかった。

このページに
UIImageView missing images in Launch Screen on device
UIImageのProperty > Drawing > Cleans Graphic Contextのチェックを外すといういう方法が載っていたが、私の場合は効かなかった。

またまた再発し、今度は画像を変えても直らない。
UIImageに設定する画像名は、プルダンウンでは各調子付きのものだが、拡張子を取ったら直ったいうのがあったので試したところ、表示されるようになった。

これでもまた再発した。
確実そうなのは画像ファイルをまだ使ったいない名前にリネームすることのようだ。XCodeでのリネームでも有効。

デバイスを再起動させたら直ったというのが幾つかあるので試してみると、一発ですんなりというわけではなかったが、これまでに使ったことのあるファイル名に戻しても画像が表示されるようになった。

iOS再起動だけでは画像が表示されるようにならない場合は、さらにアプリをアンインストー、再インストールを行う。

どうやら、iOSがアプリ起動時の画面をどこかにキャッシュしているのではないかと思われる。画面全体の画像としてのキャッシュではなくstoryboardから生成されるオブジェクト群のキャッシュだとすると、その中のUIImageのキャッシュ画像が再現できず、同じリソース名だと、iOSのキャッシュを無効にするまで表示されなくなるのかもしれない。

ちなみに、コーディングで何かできないかとも思ったが、Launch Screen.storyboardはアプリローディング中に表示されるものなので、これは無理でした。
参考:Execute code in Launch Screen

2016年8月24日水曜日

UIPageViewContollerについて

ページングするインタフェイスを作るためにUIPageViewContollerが用意されており、UIScrollViewで作りこむよりもページ切り替えの実装部分が不要なため簡潔に作れます。
しかし、TransitionStyleの初期値は"Page Curl"となっており、動作もこれに最適化されているようです。
"Scroll"も用意されていますが、この場合はちょっと思うようにいかない、カスタマイズが面倒、ということがあったのでまとめておきます。

1.UIPageViewContollerの部品構成


UIPageViewContoller
 ┗ _UIPageViewControllerContentView
  ┗ _UIQueuingScrollView
    ┗ UIPageControl

UIViewControllerのviewのサブビューにUIScrollViewとUIPageControlがある。(実際はその間にUIViewが配置されている。)

UIPageControlは、TransitionStyleが”Scroll”で、UIPageViewControllerDataSourceの次の二つのメソッドが実装されている時に追加される。
TransitionStyleの初期値は"Page Curl"なので注意。

- presentationCountForPageViewController:
- presentationIndexForPageViewController:

UIPageControlを使用しない場合はUIScrollViewは画面サイズと同じ。NavigationBarがある場合も画面サイズとなり、NavigationBarの下に隠れる部分ができる。

UIPageControlを使用する場合は画面下にUIPageControlのエリアが取られ、UIScrollViewはそれを除いたサイズになる。

2.最初のページの表示

UIPageViewContollerはスクロールに応じて前後の画面を要求するメソッドを呼び出すが、最初の画面はこれとは別に設定する必要がある。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self setViewControllers:@[someViewController]
                   direction:UIPageViewControllerNavigationDirectionForward
                    animated:NO
                  completion:nil];
}

3.ページ切替

UIPageViewContollerが表示する各ページにUIViewContollerを割り当てる。各ページには、そのviewが表示される。
各ページに異なるUIViewContollerのサブクラスを割り当てても良いが、例えば画像だけを入れ替えるような場合は同じ構成のUIViewContollerのサブクラスの複数インスタンスを割り当てたい。

AppleのPageContolサンプルで使われている方法を応用して、次のような実装をしてみた。

@interface MyPageViewController : UIPageViewController
                 <UIPageViewControllerDataSource>
//ViewControllerをキャッシュするArray
@property (nonatomic, strong) NSMutableArray *viewControllersCache;
@property (nonatomic) int numPages;
@end

@implementation MyPageViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.dataSource = self;
    //NSMutableArrayを作り、ページ数分のMyContentViewControllerオブジェクトをセットする。実際の画像セットはMyContentViewController表示時に行う。
    self.viewControllersCache = [[NSMutableArray alloc] init];
    //Photoライブラリの画像をセットするなど
    for (int n = 0; n < 3; n++) {
        self.numPages++;
//storyboardでUIViewContollerを追加し、classをMyContentViewController、Storuboard IDを"pageContent"に設定しておく。
        MyContentViewController *vc = [[self storyboard] instantiateViewControllerWithIdentifier:@"pageContent"];
        vc.pageNumber = n;
        [self.viewControllersCache addObject:vc];
    }
    [self setViewControllers:@[self.viewControllersCache.firstObject]
                   direction:UIPageViewControllerNavigationDirectionForward
                    animated:NO
                  completion:nil];
}

//今表示されているviewControllerの次のviewControllerを返す。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
       viewControllerAfterViewController:(NLInfoContentViewController *)viewController
{
    if (viewController.pageNumber >= self,numPages - 1) return nil;
    return [self.viewControllersCache objectAtIndex:viewController.pageNumber + 1];
}

//今表示されているviewControllerの前のviewControllerを返す。
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController
      viewControllerBeforeViewController:(NLInfoContentViewController *)viewController
{
    if (viewController.pageNumber <= 0) return nil;
    return [self.viewControllersCache objectAtIndex:viewController.pageNumber - 1];
}
@end

//UIImageViewを表示するViewController。
@interface MyContentViewController : UIViewController
//MyPageViewContrllerでインスタンス作成時にページ番号をセット
@property (nonatomic) int pageNumber;
//InterfaceBuilderでMyContentViewController内にUIImageを追加、コネクトする。
@property (nonatomic, weak) IBOutlet UIImageView * imageView;
@end

@implementation MyContentViewController
//viewWillAppearでUIImageを作る。
- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    if (self.imageView.image == nil)  {
        [self setImage];
    }
}

- (void)setImage
{
        NSString *imgName = [NSString stringWithFormat:@"img%d", self.pageNumber];
self.imageView.image = [UIImage imageNamed:imgName];
    //UIImageがself.imageViewのframeにリサイズされるので、必要に応じてself.imageViewのframeを変更する。
    //最初のページのサイズ調整を行うのはviewDidLayoutSubviewsのタイミングが良い。
}

@end

4.背景色設定

各ページの内容を表示するUIViewControllerのviewが1ページ分のエリアを覆うので、この背景色が優先される。これはInterfaceBuilderで設定可能。デフォルトはwhiteColor。

UIViewControllerの背景色をclearColorにした時はUIPageViewControllerのviewの背景色が有効になる。これはデフォルトでは未設定(透明)なので、見た目は黒になる。
この間にUIScrollViewがあるが、この背景色も未設定(透明)。

UIPageViewControllerのviewの属性はInterfaceBuilderでは設定できないので、コーディングで行う。

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor greenColor];
}

5.UIPageControlの表示

TransitionStyleを"Scroll"に設定し、次の二つのメソッドを実装する。

//実際のページ数(以上)の数を返す。
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
{
    return self.numPages;
}

//初期表示ページに対応するindexを返す。
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
    return 0;
}

UIPageControlは画面下に表示され、各ページの表示領域はこれを除いた部分になる。
UIPageControlの背景色はclearColorで、その下のUIPageViewControllerの背景色(透明=)が表示される。

6.UIPageControlの属性変更(位置変更以外)

UIPageViewControllerが表示するUIPageControlの属性はInterfaceBuilderでは変更できない。
コーディングで変更する場合は次の要領で行う。

- (void)viewDidLoad
{
    [super viewDidLoad];
    [UIPageControl appearance].pageIndicatorTintColor = [UIColor grayColor];
    [UIPageControl appearance].currentPageIndicatorTintColor = [UIColor greenColor];
    [UIPageControl appearance].backgroundColor = [UIColor blueColor];
}

viewDidLoad以外にviewWillAppear, viewWillLayoutSubviewsなどでも有効だが、viewDidLayoutSubviewsでは遅すぎる。

7.UIScrollView、UIPageControlのframe変更

UIPageControlを表示するとUIScrollViewのサイズが小さくなる。フルサイズ表示したい場合はリサイズが必要になる。
いささか無理やりだが、UIPageViewControllerのサブビュー階層をたどってUIScrollViewオブジェクトを探し、そのframeを変更することでリサイズすることができる。
UIPageControlの位置変更も同じ要領で可能。
リサイズが有効なのはviewDidLayoutSubviewsのタイミング。それ以前にframeを設定しても、サブビューのレイアウト時にUIPageControlが再計算してしまう。

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    UIScrollView *sv = [self findViewOfClass:[UIScrollView class] fromView:self.view];
    sv.frame = self.view.frame;
    
    UIPageControl *pc = [self findViewOfClass:[UIPageControl class] fromView:self.view];
    pc.frame = CGRectMake(0, 100, pc.frame.size.width, pc.frame.size.height);
}

特定のクラス(またはそのサブクラス)のインスタンスを探すメソッド

- (id)findViewOfClass:(Class)class fromView:(UIView *)view
{
    if ([view isKindOfClass:class]) return view;
    for(UIView *subview in view.subviews) {
        if ([subview isKindOfClass:class]) {
            return subview;
        }
        id v = [self findViewOfClass:class fromView:subview];
        if (v) return v;
    }
    return nil;
}

8.UIPageViewContollerの縦または横固定

UIPageViewContollerDelegateに次のメソッドがあるが、これを実装しただけでは縦・横固定にならない。

- pageViewControllerPreferredInterfaceOrientationForPresentation:
- pageViewControllerSupportedInterfaceOrientations:

UIPageViewContollerを表示するUINavigationControlに次のようなメソッドを追加する。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

UINavigationControlがいくつかのViewControllerを扱う場合は、次のようにするとViewController別に縦・横の扱いを変えることができる。

- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
    return [self.visibleViewController supportedInterfaceOrientations];
}

2016年8月11日木曜日

UITableViewのヘッダに大文字+小文字混じりの英字を表示する方法

UITableViewのヘッダにテキストを設定する場合は
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
で文字列を返します。

このままだと、英字部分が全て大文字に変換されます。
iOSがIOSとなるなど、変換して欲しくない、という場合がありえます。

この場合、この後で呼ばれる
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
で文字列を再設定すると、大文字+小文字混じりの表示も可能になります。

ヘッダ部分の表示位置はtitleForHeaderInSectionが返す文字列から計算されるので、willDisplayHeaderViewで再設定する場合も、ここで適切な文字列を返しておく必要があります。

PHPhotoLibrayのrequestAuthorizationでプロンプトが表示されない

PHPhotoLibrayの

+ (void)requestAuthorization:(void (^)(PHAuthorizationStatus status))handler
でアクセス許可を要求するプロンプトが表示されない。

どうやらiOS9から発生しているようです。

Info.plistにCFBundleDisplayNameを設定すれば表示されるようになると書かれているページが見つかりますが、これでもダメ。

正式ドキュメントではないようでが、一旦ユーザが設定した後はプロンプトを表示しないとのこと。

別のことで、1日一回だけチェックするので、テスト時に時計を進めろ、というものがあったので、試しに時計を進めてみたらプロンプトが表示されるようになった。

デバッグ中の場合の手順

  1. XCodeでのデバッグを終了させる。
  2. 設定で、対象アプリの写真へのアクセスをオフにする。
  3. 対象アプリをアンインストール(削除)する。
    時計を進めるだけではダメ。再検証していないが、日付を変えた後でアンインストールしてもうまくいかなった。
  4. 設定 > 一般 > 日付と時刻 で日付を1日進める。
    自動設定をオフにす。
    一旦設定すると設定した日時が記録されるようで、何度もテストする場合はその度にさらに1日進める必要がある。
  5. XCodeでデバッグを開始する。


requestAuthorization実行で写真へのアクセスを要求するプロンプトが表示される。

このプロンプトは非同期で実行され、プロンプトを閉じたことがわからないので、アプリ側で何らかの再実行手段を講じる。
もしかしたら、何らかの通知機能があるかもしれない。

プロンプトでOKをタップした場合は正常処理を進めることができるが、キャンセルがタップされた場合は写真へのアクセスが禁止されたままの状態となる。この状態で再度requestAuthorizationを実行しても(おそらく24時間経過するまでは)プロンプトは再度表示されない。そのため、設定で写真へのアクセスを許可するように、ユーザに知らせる必要がある。

また、iOS10からinfo.plistにNSPhotoLibraryUsageDescriptionを設定しておかないとエラーが発生するので、これも設定しておく。

2015年10月9日金曜日

NavigationBar非表示の左エッジスワイプ


UINavigationControllerは左端からのスワイプで前の画面に戻るが、NavigationBar非表示だと戻らなくなる。画面を覆っているViewがTouchイベントを吸収してしまうからのようです。

その場合は、そのViewにUIScreenEdgePanGestureRecognizerをセットして、自分で前の画面に戻してやればいい。
  • UIViewControllerにUIScreenEdgePanGestureRecognizerをドラッグ&ドロップで追加。
  • ViewからControl+ドラッグでUIScreenEdgePanGestureRecognizerをコネクト。
  • gestureRecognizersに追加する。

  • UIScreenEdgePanGestureRecognizerのプロパティーでLeftをチェック。

  • ViewのプロパティーでUser Interaction Enabledをチェック。

ソースコードの例
最低限の追加はUIScreenEdgePanGestureRecognizerのアクションを受けとるメソッド。
前の画面に戻るので、UIViewControllerに実装するのがよいでしょう。
- (IBAction)handleLeftEdgeGesture:(id)gesture
{
    [self.navigationController popViewControllerAnimated:YES];
}

これをIBでUIScreenEdgePanGestureRecognizerのアクションにセットする。

delegateメソッドは必須ではないが、必要な場合は<UIGestureRecognizerDelegate>をヘッダに追加し(これもなくても動いてしまうが)、適宜delegateメソッドを実装。

ステイタスバーを隠す場合は、UIViewControllerに次のメソッドを追加。
- (BOOL)prefersStatusBarHidden
{
    return YES;
}

NavigationBarを隠すにはUIViewControllerのviewDidLoadなどに次のメソッドを追加。
    [self.navigationController setNavigationBarHidden:YES];

これを行ったあとは前の画面に戻ったときもNavigationBarが隠れたままになるので、そちらでsetNavigationBarHidden:NOに戻す。

ちなみに、UINavigationControllerの左端からのスワイプ動作が都合の悪い時は、次のメソッドで無効にできる。

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}

2015年10月1日木曜日

SDメモリにコピーしたXcodeが起動できない

MacBook Air 128Gの残り容量が少なくなり、Xcodeをアップデートするときに以前のバージョンを残しておくスペースがない。そこで、SDメモリにコピーしておいて、そこからも起動可能にしておこうと、試してみました。

すると、こんなエラーが発生。
Xcode is running from a volume that does not support ownership. Please move Xcode to one that does.

ちなみにUSBメモリではこのエラーは発生しませんでした。

Finderで当該SDを選択 => 情報を見る
ダイアログの下の方の鍵を外し、「このボリューム上の所有権を無視」のチェックを外す。
こんなコマンドでも良いようです。
sudo diskutil enableOwnership /dev/disk0s2

再度起動を試すと別のエラーが発生。
/Volumes/...../Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/dyld_sim is not owned by root.

ターミナルで次のコマンドを実行。

ls -la /Volumes/

目的のXcodeのフォルダを探し、次のコマンドを実行。
sudo chown -R -v root xcpde-app-path

これで起動できるようになりました。ヘビーな使い方は無理でしょうが、以前はどうだったかな?と思ったときには役立つでしょう。

参考:

2015年9月29日火曜日

AppSotreからのXocdeアップデートが失敗する

追記:AppSotreからのXocde 8へのアップデートが失敗する

アップデートの度に容量不足が発生するが、8へのアップデートでは20Gほど空いていないとアップデートできない。

ディスク使用量をチェックしたら~/Library/Developer/Xcode/iOS DeviceSupportに古いバージョンのものが残っていおり、大量にスペースを消費していた。
これらはシンボルが入っているだけで、必要であればXcodeがデバイスからダウンロードするという書き込みがあったので、古いものを削除した。


AppSotreからのXocde 7へのアップデートが失敗する

アップデート失敗後に「”購入済みページ”からダウンロードし直してください。」とメッセージが表示される。
そのとおりにしてみても、ダウンロード完了直前に同じことが再発する。

結果的にディスクの残容量不足でした。9G程度必要なようです。
このエラーが発生したときに、どうも途中までダウンロードしたものが残っているようで、残容量を更に圧迫していたようです。

途中でAppStoreにDebugメニューを表示し、Reset Applicationを行っています。もしかしたら、これも効果があったのかもしれません。

Debugメニューを表示するにはタミーナルから次のコマンドを実行します。
defaults write com.apple.appstore ShowDebugMenu -bool true

追加:iMovieアップデートでも同様のことが発生。なんどかアップデートを試しても失敗。AppStoreのReset Application後に成功した。

参考:
[Sy] Xcodeアップデート時に「”購入済みページ”からダウンロードし直してください。」とエラーになる場合の対処方法
Xcode failed to download. Use the Purchases page to try again


おまけ:"アプリケーション"フォルダのサイズがえらく大きくなっていたので調べたところ、Chromeが古いバージョンが残っていた。古いバージョンを消しても今のところ問題なし。

- /Applications/Google Chromeを右クリック
- "パッケージの内容を表示"を選択
- Contents/versionsへ移動
- 古いバージョンを削除


ビルド時のsdkのディレクトリが見つからないエラー
  • Project選択
  • Build Settings
  • Framework Search Paths検索
  • 該当する設定を削除
私の場合はTest Projectにこのパスが含まれていました。

Directory not found for option '-F/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.0.sdk/Developer/Library/Frameworks'

参考:
Getting Framework related warning in Xcode 7.0

Blockの引数エラー

エラーメッセージ
/Users/my/Desktop/Proj/WebTest/WebTest/NLWebView.m:103:35: Conflicting parameter types in implementation of 'webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:': 'void (^ _Nonnull __strong)(void)' vs 'void (^__strong _Nonnull)()'

後ろのほうの型宣言部分の
void (^__strong _Nonnull)()'
は、実際には
void (^)()'
となっています。

必要な修正は
void (^)(void)'
とすることですが、メッセージ通りに
void (^ _Nonnull __strong)(void)
としておいた方が無難かもしれません。

iOS Simulatorエラー

WKWebViewのテストプログラムをiOS Simulatorで何度か起動していたら、次のエラーが発生して、再起動できなくなった。

The operation couldn’t be completed. FBSOpenApplicationErrorDomain Code=3

iOSシミュレータの次のメニューからコンテンツをリセットする。
[iOS Simulator]-[Rest Contents and Settings...]-[Reset]

参考:
Unable to run app in Simulator エラーの対応方法


WKWebViewのhtmlロードでエラー発生

こんなエラーが発生。
The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.

Xcodeをアップデートしたらメッセージが変わった?
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

これまでのコーディングでレスポンスが戻らず無反応状態になっている場合は
- (void)webView:didFailProvisionalNavigation:withError:
を実装し、errorをチェックしてみる。

iOS9からApp Transport Security(ATS)が導入され、デフォルトではHTTPでの通信ができなくなっている。
App Transport Security policyはplistで変更することが可能で、従来の動作に戻すには次の設定を追加する。

<key>NSAppTransportSecurity</key>  
     <dict>  
          <key>NSAllowsArbitraryLoads</key><true/>  
     </dict>  

不特定なサイトにアクセスする必要がある場合は、この設定にするしかなさそう。
特定のサイトに限定できる場合はドメイン指定でアクセス可能にする。

参考:
これに関連したGoogleのページもありました。

The launch image set "LaunchImage" has 5 unassigned children.

どうやら、以前のProjectでlaunch image setを2つ作ってしまったようです。

Project選択
 => Target
 => App Icons and Launch Images
 => Launch Image Sources

のプルダウンリストを見ると"Launchimage"が選択状態になっているが、もうひとつLaunchimage−1がある。
こちらに変更すると以前の状態にもどった。

もともとのLaunchimageが適切に設定されておらず、別のLaunchimageを追加し、そちらを使っていた。Xcode7は元の名前の方を選択状態にしたため、ということのようです。


画面がフルスクリーンにならない

前項の続き。
Imageが適切に設定できていないと、Default@2x(640x960)のサイズが適用され、そのイメージサイズがWindowサイズに適用されているようだ。そのため、画面がフルスクリーンにならない。

iOS8以降のみのサポートであればLaunch ImageにStoryboadまたはxibを使うことができるが、iOS7以前のサポートが必要な場合は従来のstatic imageを使う必要がある。
新しいImage Setを追加すると、iPhone6(Plus)のイメージも割り当てらるようになる。

Project選択
 => Target
 => App Icons and Launch Images
 => Launch Image Sources
で設定を表示する。

画面上部がiPhone Portrait iOS8,9 / iPhone Landscape iOS 8,9になっていない場合は 右クリック => New Image Set で追加。
Launch Imagesを再設定する。古いImage Setは削除可能。
App Icons and Launch Images => Launch Image Sourcesのプルダウンで、追加したImage Setを選択する。

各々のイメージフォルダには適切なサイズのImageを割り当てないと有効にならない。
その場合、こんなWarningが出る。
launchimage/Default-568h@2x-1.png is 640x1136 but should be 750x1334.

また、Xcodeから起動するデバイスに適用可能なImageがひとつもないと、次のエラーが発生する。
The launch image set named "LaunchImage-1" did not have any applicable content.

そのデバイスに対応するImageが未設定あるいは不適切でも、適用可能なものがあればそれが割り当てられる。

たとえばiPhone 6用だけにiPhone5のImageをセットすると上のエラーになる。その状態でiPhone 5用のImageもセットするとエラーは消え、iPhone5のサイズが適用される。

参考:
Launch Image not showing up in iOS application (using Images.xcassets)

iOS 6もサポートする場合はこんなエラーもでる。

iPhone Retina (4-inch) launch image for iOS 6.x and prior is required when targeting releases prior to 7.0.

この場合は、こちらをご覧ください。

marble seijin の開発日記 まだまだある、xcode5での変更点


2015年9月26日土曜日

JQueryMobile listview サンプル 言語切り替え付き

jQuery Mobileのlistviewを使ったサンプルです。


JQueryMobile listview サンプル に言語切り替えを加えたものです。
デモサイト

言語切り替えなしバージョンと基本的に同じですが、表示文字列を"jp"、"en"のペアで設定し、言語選択に応じて表示を切り替えます。
言語選択はメインメニュー画面のラジオボタンでJapanese、Englishを選択することができます。

HTML一式ダウンロードできます。

index.html のタグ構造

<body onload="init();">
          <div data-role="page" id="topPage">
            <div data-role="header">
                <h1 class="headerText" id="topPageHeaderText">ホームページ</h1>
            </div>
            <!-- メインメニューを埋め込むdiv -->
            <div id="topMenu"></div>
            <!-- 言語切り替えラジオボタン -->
            <div id="langDiv">
                <input type="radio" name="lang" id="radio_jp" onclick="setLang('jp');">
                    <label for="radio_jp" id="label_jp">Japanese</label>
                <input type="radio" name="lang" id="radio_en" onclick="setLang('en');">
                    <label for="radio_en" id="label_en">English</label>
            </div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>
        
        <!-- メインメニューのページ1をタップしときに遷移するpage。サブメニューを表示する。 -->
        <div data-role="page" id="subMenuPage">
            <div data-role="header" data-add-back-btn="true" data-back-btn-text="戻る" onclick="return backClicked(this);">
                <h1 class="headerText">subMenuPage</h1>
            </div>
            <!-- サブメニューを埋め込むdiv -->
            <div id="subMenu"></div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>

        <!-- コンテンツ表示。メインメニューのページ3、およびサブメニューをタップしときに遷移しコンテンツを表示するpage。 -->
        <div data-role="page" id="contentPage" >
            <div data-role="header" data-add-back-btn="true" data-back-btn-text="戻る" onclick="return backClicked(this);">
                <h1 class="headerText">contentPage</h1>
            </div>
            <div data-role="main" class="ui-content" id="iframeDiv">
                <!-- コンテンツをロードするiframe -->
               <iframe id="iframe"></iframe>
            </div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>
    </body>

言語設定javascipt

//言語設定。ブラウザの言語設定が"ja"なら日本語、さもなければ英語。
            //index.htmlにハッシュ値がある場合は"ja"なら日本語、さもなければ英語。
            //index.htmlアクセス時にハッシュ値で言語設定可能。ハッシュ値をLangにセットする。
            //index.html#jpまたはindex.html#enでアクセス。
            var Lang = (navigator.language.indexOf("ja") < 0 ? "en" : "jp");
            if (location.hash.length > 0) {
                Lang = (location.hash.substr(1) == "jp" ? "jp" : "en");
            }
var Lang = location.hash.length == 0 ? "jp" : location.hash.substr(1);

//文字列のjp、enペアデータ。メインメニューのヘッダ、Backボタンの表示名。
           var label = {
                    "topPageHeader": {"jp":"ホームページへようこそ", "en":"Welcome to My Homepage"},
                    "backBtnText": {"jp":"戻る", "en":"Back"}
                };
//言語切り替えラジオボタンタップ/クリックで呼ばれ、メインメニューを再設定する。
           function setLang(lang) {
                Lang = lang;
                setMenu('topMenu', 'topMenuList');
           }

index.htmlのonloadイベントハンドラ

//body.onloadで呼ばれる。iframeのheight設定。
            //ブラウザのreloadボタンクリック時はindex.htmlをリロードする。
            function init() {
                if (document.getElementById("topMenuList") == null || $('#topMenuList').length == 0) {
                    location = "index.html";
                } else {
                    var h = $(window).height() - 20;
                    $('#iframe').css("height", h + "px");
                }
                $('#radio_' + Lang).prop('checked', true);
                $('input[name=lang]').checkboxradio('refresh');
            }

メインメニューを生成するjavascript

            //メインメニューのデータ。
            //text: リスト表示名
            //page: 遷移先ページ ID
            //src: iframeにロードするhtml、またはサブメニューのjson変数名。
            var topMenuList = [
                    {"jp":"ページ1 - サブメニュー", "en":"Page1 - SubMenu", "page":"#subMenuPage", "src":"subMenuList1"},
                    {"jp":"ページ2 - サブメニュー", "en":"Page2 - SubMenu", "page":"#subMenuPage", "src":"subMenuList2"},
                    {"jp":"ページ3 - コンテンツ", "en":"Page1 - Contents", "page":"#contentPage", "src":"Contents/page3"}
                    ];

            //index.htmlロード時に呼ばれ、メインメニューを生成。
            $(document).on('pagebeforeshow', '#topPage', function(){
                if ($('#topMenuList').length == 0) {
                    setMenu('topMenu', 'topMenuList');
                }
            });

メインメニュータップ/クリックで呼ばれサブメニューを生成するjavascript

//サブメニューのデータ。項目はメインメニューと同じ。 var subMenuList1 = [ {"jp":"1. 項目11", "en":"1. Item11", "page":"#contentPage", "src":"Contents/item11"}, {"jp":"2. 項目12", "en":"2. Item12", "page":"#contentPage", "src":"Contents/item12"}, {"jp":"3. 項目13", "en":"3. Item13", "page":"#contentPage", "src":"Contents/item13"} ]; //サブメニューのデータ。項目はメインメニューと同じ。 var subMenuList2 = [ {"jp":"1. 項目21", "en":"1. Item21", "page":"#contentPage", "src":"Contents/item21"}, {"jp":"2. 項目22", "en":"2. Item22", "page":"#contentPage", "src":"Contents/item22"}, {"jp":"3. 項目23", "en":"3. Item23", "page":"#contentPage", "src":"Contents/item23"} ]; //サブメニュー生成。異なるサブメニューで同じDIV構成を強要するため、表示のたびにlistviewを作り直す。 function setMenu(menuDivId, menuDataName) { if (menuDivId == "topMenu") { $('#topPageHeaderText').text(label.topPageHeader[Lang]); } var menuListId = '#' + menuDivId + 'List'; var ul = $(menuListId); if (ul.length > 0) { ul.empty(); } else { ul = $('<ul>').attr({'id':menuDivId + 'List','data-role':'listview'}).appendTo('#' + menuDivId); } var menuItemList = eval(menuDataName); for(var i=0; i < menuItemList.length; i++) { var li = $('<li>').appendTo(menuListId); var item = menuItemList[i]; $('<a data-transition="slide" href="' + item.page + '">') .attr('onclick', 'linkClicked("' + item.page + '","' + item[Lang] + '","' + item.src + '")') .text(item[Lang]).appendTo(li); } $(menuListId).listview().listview('refresh'); }

メニュー項目タップ/クリックで呼ばれ画面遷移を行うjavascript

//画面遷移の履歴。 //iframeを使用するとBackボタンを二度クリック/タップしないと画面遷移しなくなるため、独自に履歴を管理する。 var pageArray = new Array(""); //listviewの項目がクリック/タップされたときに呼ばれる。 //遷移先のヘッダのタイトル設定。現在のページをpageArrayにpushする。 function linkClicked(page, title, src) { if (page == "#subMenuPage") { setMenu("subMenu", src); } else if (src.length > 0) { $('#iframe').attr('src', src + '_' + Lang + '.html'); } $(page).find('.headerText').text(title); //'data-back-btn-text'を変更しても要素が再作成されるわけではないので表示が変わらない。 //refreshする方法が見つからなかったのでボタンのaタグのtextを直接変更する。 //$(page).find('div[data-role="header"]').attr('data-back-btn-text', label.backBtnText[Lang]); $(page).find('a[role="button"]').text(label.backBtnText[Lang]);
                pageArray.push(findParentPage($(event.srcElement)));
            }
        
            //paretnElementを辿り、data-role="page"の要素を返す。見つからない場合はnullを返す。
            function findParentPage(elm) {
                if (elm == null || elm.attr('data-role') == 'page') return elm;
                return findParentPage(elm.parent());
            }

戻るボタンで画面遷移を行うjavascript

//Backボタンタップ/クリック。 //戻り先pageIdがあればchangePageをよびfalseを返す。さもなけばtrueを返し、frameworkに任せる。 function backClicked(elm) { var page = pageArray.pop(); if (page == null) return true; var pageId = page.attr('id'); if (pageId.length == 0) return true; $.mobile.changePage('#'+pageId, { transition: "slide", reverse: true } ); return false; }
参照
JQueryMobile listview サンプル
HTML5+JavaScriptで画面をスライドさせて切り替える方法

JQueryMobile listview サンプル

jQuery Mobileのlistviewを使ったサンプルです。

自前で作ったメニュー表示/画面スライドをJQueryMobileで作り直したものです。
デモサイト
日本語/英語の言語切り替え付きもあります。

メインメニュー/サブメニュー/コンテンツといった多階層の画面遷移を組み込んでいます。
メニューのlistviewをjsonデータからjavascriptで動的に生成し、コンテンツ表示部分は各項目で共有し、コンテンツはiframeに外部のhtmlを読み込む構成になっています。
これにより画面遷移に関わる部分のhtmlがシンプルになり、コンテンツのメンテナンスも容易になります。

iframeを使っているため、backボタンの動作に手を加えています。これが最善かどうかはわかりませんが、ともかくも期待通りに動作しています。

HTML一式ダウンロードできます。

index.html のタグ構造

<body onload="init();">
        <div data-role="page" id="topPage">
            <div data-role="header">
                <h1 class="headerText">ホームページ</h1>
            </div>
            <!-- メインメニューを埋め込むdiv -->
            <div id="topMenu"></div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>
        
        <!-- メインメニューのページ1をタップしときに遷移するpage。サブメニューを表示する。 -->
        <div data-role="page" id="subMenuPage">
            <div data-role="header" data-add-back-btn="true" data-back-btn-text="戻る" onclick="return backClicked(this);">
                <h1 class="headerText">subMenuPage</h1>
            </div>
            <!-- サブメニューを埋め込むdiv -->
            <div id="subMenu"></div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>

        <!-- コンテンツ表示。メインメニューのページ3、およびサブメニューをタップしときに遷移しコンテンツを表示するpage。 -->
        <div data-role="page" id="contentPage" >
            <div data-role="header" data-add-back-btn="true" data-back-btn-text="戻る" onclick="return backClicked(this);">
                <h1 class="headerText">contentPage</h1>
            </div>
            <div data-role="main" class="ui-content" id="iframeDiv">
                <!-- コンテンツをロードするiframe -->
               <iframe id="iframe"></iframe>
            </div>
            <div data-role="footer" class="footer">
                <h6 class="footerText">Footer Text</h6>
            </div>
        </div>
    </body>

index.htmlのonloadイベントハンドラ
//body.onloadで呼ばれる。iframeのheight設定。
            //ブラウザのreloadボタンクリック時はindex.htmlをリロードする。
            function init() {
                if (document.getElementById("topMenuList") == null || $('#topMenuList').length == 0) {
                    location = "index.html";
                } else {
                    var h = $(window).height() - 20;
                    $('#iframe').css("height", h + "px");
                }
            }

メインメニューを生成するjavascript
//メインメニューのデータ。
            //text: リスト表示名
            //page: 遷移先ページ ID
            //src: iframeにロードするhtml、またはサブメニューのjson変数名。
            var topMenuList = [
                    {"text":"ページ1 - サブメニュー", "page":"#subMenuPage", "src":"subMenuList1"},
                    {"text":"ページ2 - サブメニュー", "page":"#subMenuPage", "src":"subMenuList2"},
                    {"text":"ページ3 - コンテンツ", "page":"#contentPage", "src":"Contents/page3.html"}
                    ];

            //index.htmlロード時に呼ばれ、メインメニューを生成。
            $(document).on('pagebeforeshow', '#topPage', function(){
                if ($('#topMenuList').length == 0) {
                    var ul = $('<ul>').attr({'id':'topMenuList','data-role':'listview'}).appendTo('#topMenu');
                    for(var i=0; i < topMenuList.length; i++) {
                    var li = $('<li>').appendTo('#topMenuList');
                    var item = topMenuList[i];
                    $('<a data-transition="slide" href="' + item.page + '">')
                        .attr('onclick', 'linkClicked("' + item.page + '","' + item.text + '","' + item.src + '")')
                        .text(item.text).appendTo(li);
                    }
                    $('#topMenuList').listview().listview('refresh');
                }
            });


メインメニュータップ/クリックで呼ばれサブメニューを生成するjavascript
//サブメニューのデータ。項目はメインメニューと同じ。
            var subMenuList1 = [
                   {"text":"1. 項目11", "page":"#contentPage", "src":"Contents/item11.html"},
                   {"text":"2. 項目12", "page":"#contentPage", "src":"Contents/item12.html"},
                   {"text":"3. 項目13", "page":"#contentPage", "src":"Contents/item13.html"}
                   ];

            //サブメニューのデータ。項目はメインメニューと同じ。
            var subMenuList2 = [
                   {"text":"1. 項目21", "page":"#contentPage", "src":"Contents/item21.html"},
                   {"text":"2. 項目22", "page":"#contentPage", "src":"Contents/item22.html"},
                   {"text":"3. 項目23", "page":"#contentPage", "src":"Contents/item23.html"}
                   ];

            //サブメニュー生成。異なるサブメニューで同じDIV構成を共用するため、表示のたびにlistviewを作り直す。
            function setMenu(name) {
                var ul = $('#subMenuList');
                if (ul.length > 0) {
                    ul.empty();
                } else {
                    ul = $('<ul>').attr({'id':'subMenuList','data-role':'listview'}).appendTo('#subMenu');
                }
                var menuList = eval(name);
                for(var i=0; i < menuList.length; i++) {
                    var li = $('<li>').appendTo('#subMenuList');
                    var item = menuList[i];
                    $('<a data-transition="slide" href="' + item.page + '">')
                    .attr('onclick', 'linkClicked("' + item.page + '","' + item.text + '","' + item.src  + '")')
                    .text(item.text).appendTo(li);
                }
                $('#subMenuList').listview().listview('refresh');
            }

メニュー項目タップ/クリックで呼ばれ画面遷移を行うjavascript
            //サブメニューのデータ。項目はメインメニューと同じ。
            //画面遷移の履歴。
            //iframeを使用するとBackボタンを二度クリック/タップしないと画面遷移しなくなるため、独自に履歴を管理する。
            var pageArray = new Array("");
            
            //listviewの項目がクリック/タップされたときに呼ばれる。
            //遷移先のヘッダのタイトル設定。現在のページをpageArrayにpushする。
            function linkClicked(page, title, src, backTo) {
                if (page == "#subMenuPage") {
                    setMenu(src);
                } else if (src.length > 0) {
                    $('#iframe').attr('src', src);
                }
                $(page).find('.headerText').text(title);
                pageArray.push(findParentPage($(event.srcElement)));
            }
        
            //paretnElementを辿り、data-role="page"の要素を返す。見つからない場合はnullを返す。
            function findParentPage(elm) {
                if (elm == null || elm.attr('data-role') == 'page') return elm;
                return findParentPage(elm.parent());
            }

戻るボタンで画面遷移を行うjavascript
            //Backボタンタップ/クリック。
            //戻り先pageIdがあればchangePageをよびfalseを返す。さもなけばtrueを返し、frameworkに任せる。
            function backClicked(elm) {
                var page = pageArray.pop();
                if (page == null) return true;
                var pageId = page.attr('id');
                if (pageId.length == 0) return true;

                $.mobile.changePage('#'+pageId, { transition: "slide", changeHash: true, reverse: true } );
                return false;
            }

参照
JQueryMobile listview サンプル 言語切り替え付き
HTML5+JavaScriptで画面をスライドさせて切り替える方法