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>

2012年6月23日土曜日

HTML5+JavaScriptによるパノラマ写真実装例



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

360°パノラマ写真をスワイプで回転させるサンプルアプリです。
水平方向のみの対応です。
デスクトップではマウスドラッグで回転します。IEでもヘッダのレイアウトが崩れますが、回転動作はします。

このアプリではManifestは設定していますが、LocalStorageは使用していません。

このサンプルアプリのパノラマ写真回転に関するコードは以下のとおりです。

メニュー画面で[Panorama]タップ時に表示するDIV
    <div id="divPic" class="pic"></div>

スタイルシート
     div.pic {
width:100%;
         height:220px;
background-position-x:0px;
    }

JavaSctipt(簡略化のためiOS対応の場合のみ抽出)
    //Picオブジェクト作成
    var pic = new Pic(divPic, "Images/001.jpg");
    //Picクラス
    Pic = function(div, url) {
        this.div = div;
        this.url = url;
        //background-imageセット
        div.style.backgroundImage = 'url("' + url + '")';
        //swipeまたはdrag操作のためのイベントハンドラ
        div.addEventListener("touchstart", Pic.touchStart, false);
        Pic.dict[div.id] = this;
    }
    //DIVのidをkey、valueをPicオブジェクトとするArray
    Pic.dict = new Array();
    //移動量計算のため直前のtouch位置をセット. 初期値=Number.MIN_VALUE
    Pic.prevX = Number.MIN_VALUE;
    //イベントハンドラから対応するPicオブジェクトを探す.
    Pic.getInstance = function(id) {
        return Pic.dict[id];
    }
    //touch 開始
    Pic.touchStart = function(event) {
        Pic.prevX = Number.MIN_VALUE;
        var x = event.srcElement.style.backgroundPositionX;
        var div = event.srcElement;
        //イベントハンドラ設定
        div.addEventListener("touchmove", Pic.touchMove, false);
        div.addEventListener("touchend", Pic.touchEnd, false);
    }
    //swipe
    Pic.touchMove = function(event) {
        var x = event.pageX;
        if (Pic.prevX == Number.MIN_VALUE) {
            //一回目はprevXをセット
            Pic.prevX = x;
        } else {
            //二回目以降
            var pic = Pic.getInstance(event.srcElement.id);
            //x位置の末尾の"px"を取り、数値化.
            var px =    pic.div.style.backgroundPositionX;
            var p = new Number(px.substr(0, px.length-2));
            p += (x - Pic.prevX);
            //swipe量を加減した位置にbackgroundImageを移動
            pic.div.style.backgroundPositionX = p + "px";
            Pic.prevX = x;
        }
    }
    //touch 終了
    Pic.touchEnd = function(event) {
        Pic.prevX = Number.MIN_VALUE;
        var div = event.srcElement;
        div.removeEventListener("touchmove", Pic.touchMove);
        div.removeEventListener("touchend", Pic.touchEnd);
    }

HTML5+JavaScriptによるパノラマ写真実装例

2012年6月18日月曜日

残りメモリ容量表示



残りメモリ容量表示

メモリ使用状況はInstrumentsでチェックすることができますが、処理が重く、またiPhoneデバイス単体でのテストが行えません。そこで、残りメモリ容量を画面に表示させるクラスを作りました。UILabelなどを使って画面の空きスペースに表示させることができます。

.hファイル

#import <Foundation/Foundation.h>

@interface MemMonitor : NSObject

@property (nonatomic, readonly) long freeMemory;
@property (nonatomic, readonly) NSString *freeMemoryAsFormattedString;

@end

.mファイル
#import "MemMonitor.h"
#import <mach/mach.h>
#import <mach/mach_host.h>

@implementation MemMonitor
{
    mach_msg_type_number_t vm_info_count;
    mach_port_t mach_port;
    vm_size_t pagesize;
    NSNumberFormatter *formatter;
}
//初期化
- (id)init
{
    self = [super init];
    if (self) {
        vm_info_count = HOST_VM_INFO_COUNT;
        mach_port = mach_host_self();
        host_page_size(mach_port, &pagesize);
        formatter = [[NSNumberFormatter alloc] init];
        [formatter setPositiveFormat:@"#,##0"];
    }
    return self;
}
//メモリ残り容量をlongで返す。
- (long)freeMemory 
{
    struct vm_statistics vm_stat;
    kern_return_t ret = host_statistics(mach_port, HOST_VM_INFO, (host_info_t)&vm_stat ,&vm_info_count);
    if (ret == KERN_SUCCESS) {
        return (NSInteger)vm_stat.free_count * pagesize;
    } else {
        return -1;
    }
}
//メモリ残り容量を3桁カンマ区切りでフォーマットしたNSStringで返す。
- (NSString *)freeMemoryAsFormattedString
{
    NSNumber *num = [NSNumber numberWithInteger:self.freeMemory];
    return [formatter stringFromNumber:num];
}
@end

使用例:
UILabel *memMonitorLabel;

MemMonitor *memMonitor = [[MemMonitor alloc] init];
memMonitorLabel.text = [memMonitor freeMemoryAsFormattedString];

2012年6月17日日曜日

DOT.NETエラー集



ASP.NETエラー集

あまりお目にかからないエラーに出会ったのをきっかけに、ASP.NET関連のエラーを集めてみます。

  • 「オブジェクトの現在の状態に問題があるため、操作は有効ではありません。」データ量が増えると発生するようであれば、コントロール数の上限を超えたために発生しました。Web.configの設定で回避できます。
    <appSettings>
    <add key="aspnet:MaxHttpCollectionKeys" value="5001" />
    </appSettings>

    参考:~ 開発者の憂鬱 ~
          
  • 型 'MyProject.MyMembershipProvider' を読み込めませんでしたMembershipProviderまたはRollProviderのサブクラスをaspxのあるプロジェクトとは別の(クラスライブラリなどの)別のプロジェクトに置いた場合に発生しました。
    aspxのあるプロジェクト内でMyMembershipProviderをコードから呼ばない場合、aspxのあるプロジェクトに(この例の場合は)MyProjectを追加しなくてもコンパイルは通りますが、実行時にWeb.configの設定に従ってMyMembershipProviderを使用するときにクラスが見るからず、エラーとなります。

    aspxのあるプロジェクトの参照設定に"MyProject"を追加することで解消されます。

  • Provider name cannot be null or empty.MembershipProviderまたはRollProviderのサブクラスをaspxのあるプロジェクトとは別の(クラスライブラリなどの)別のプロジェクトに置いた場合に発生しました。

    親クラスのMembershipProviderのNameプロパティ―が空文字を返すために発生します。

    同じプロジェクト内であればエラーとならないのはなぜかわかりませんが、次のようなコードを追加すると回避できます。

        override public string Name
    {
        get { return "MyMembershipProvider"; }
    }

    参考:Problem with custom Role Provider

  • 'System.Xml.Linq.XElement' に 'XPathSelectElement' の定義が含まれておらず、型 'System.Xml.Linq.XElement' の最初の引数を受け付ける拡張メソッドが見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。
    LINQ to XMLでXPathSelectElementを使おうとしたら発生。”using System.Xml.XPath;"を追加すると解決する。

    参考: Is there something wrong with my System.Xml.Linq library?
  • 無効なポストバックまたはコールバック引数です。イベントの検証は、構成の <pages enableEventValidation="true"/>、またはページの <%@ Page EnableEventValidation="true" %> を使用して有効にされます。...

    次のようなDataGridの中にButtonなどを組み込んだ場合に発生する場合があります。

        <asp:GridView ID="GridView1" runat="server">
            <Columns>
                <asp:TemplateField HeaderText="">
                    <ItemTemplate>
                        <asp:Button ID="Button1" runat="server" Text="Test"
                                OnCommand="button1_Clicked"
                                CommandArgument=CommandArgument='<%# Eval("ID") %>'
                                autoPsotBack="true" />

                    </ItemTemplate> </asp:TemplateField>

    これはViewStateが有効なときに、 Page_LoadのPostBackの処理でDataGridを再バインドするのが原因です。

    もし
    Page_Loadでの再バインドが必要でViewStateを無効にしてもよい場合は、PageあるいはDataGridのViewStateModeを"Disabled"にすればエラーが解消します。

    ViewStateを有効にしておく必要がある場合は次の要領でPostBack以外のときだけDataGridのバインドを行います。

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack
            {
               //この時だけDataGridのバインドを行う。
            }
        }

    DataGridの再バインドが必要な場合は、上記の例の場合はコマンドイベントで行います。

    public void button1_Clicked(object sender, EventArgs e)
        {
           //ここでDataGridのバインドを行う。
        }
    

    参考: Invalid postback or callback argument in ImageButton

  • コンパイル エラー メッセージ: CS1040: プリプロセッサ ディレクティブは行でスペース以外の最初の文字でなければなりません。

    DataGridの中のCommandArgument設定で、次のような文字結合を行うと発生します。CommandArgument='<%# Eval("item1") %>&<%# Eval("item2") %>'

    この例の場合は次のように変更すると文字結合を行えます。
    CommandArgument='<%# Eval("item1") + "&" + Eval("item2") %>'

    エラーそのものは、不適切なコードのために二番目の"#"がコンパイラのプリプロセッセのディレクティブとみなされ、これが行の途中に出現しているために発生しているようです。

2012年6月16日土曜日

iPod Library表示



Playerが表示するiPod Library画面の呼び出し方法です。
SimulatorではiPod Libraryにアクセスできないため、実機を接続する必要があります。
iPhone simulatorで実行すると次のエラーでクラッシュします。
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Unable to load iPodUI.framework'

MediaPlayer.framework追加


.hファイル
#import <MediaPlayer/MediaPlayer.h>

@interface MyViewController : UIViewController <MPMediaPickerControllerDelegate>
- (IBAction)someMethod:(id)sender;
@end

.mファイル
@implementation MyViewController

//MPMediaPickerController呼び出し.
- (IBAction)showLibrary:(id)sender {
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes: MPMediaTypeAnyAudio];
    [picker setDelegate: self];
    //複数選択を許す場合YES、一曲のみ選択の場合NO
    [picker setAllowsPickingMultipleItems: YES];
    [self presentModalViewController:picker animated: YES];
}

//Pickerの[Cancel]ボタンタップ. 
- (void)mediaPickerDidCancel: (MPMediaPickerController *)mediaPicker
{
    // モーダルビュー解除.
    [self dismissModalViewControllerAnimatedYES];
}

//Pickerの[Done]ボタンタップ.
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker
 didPickMediaItems:(MPMediaItemCollection *)collection
{
    [self dismissModalViewControllerAnimated: YES];
    [self doSomethingWithCollection: collection];
}

//collectionを使った処理
- (void)doSomethingWithCollection:(MPMediaItemCollection *)collection
{
    //MusicPlayer作成
    MPMusicPlayerController* musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
    //collectionをセット
    [musicPlayer setQueueWithItemCollection: collection];
    //演奏開始
    [musicPlayer play];
}

//曲名等の取得
MPMediaItem * mediaItem = [[collection items] objectAtIndex:index];
[mediaItem valueForProperty:MPMediaItemPropertyArtist];
[mediaItem valueForProperty:MPMediaItemPropertyAlbumTitle];
[mediaItem valueForProperty:MPMediaItemPropertyTitle];

//歌詞の取得
[mediaItem valueForProperty: MPMediaItemPropertyLyrics];

ただし、MPMediaItemの場合はPlayerで一度再生した曲でないと歌詞データが取得できない(Xcode 4.3.2)。AVAssetからは取得できる。
AVFoundation.framework追加

#import <AVFoundation/AVFoundation.h>

NSURL *url = [mediaItem valueForProperty:MPMediaItemPropertyAssetURL];
AVAsset *asset = [AVAsset assetWithURL:url];
NSString *lyrics = asset.lyrics;  

参考:iPodライブラリアクセスプログラミングガイド

this class is not key value coding-compliant for the key someKey.



reason: '[<MyClass 0x1346b0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key someKey.'

someKeyメソッド, あるいはReadOnlyでなければsetSomeKeyメソッドがない場合に発生します。
次の場合に発生しました。
  • StoryboardでActionを追加する。
  • .hにメソッド宣言、.mに実装が追加される。
  • その後に.h、.mから宣言、実装を削除したが、.storyboardに接続情報が残っている.
次のいずれかの方法で解消する。
  • Storyboardを開き当該接続がある部品のペインを表示、当該接続を解除する。

  • Storyboardをソースで開き、当該Actionの記述を削除する。
    <connections>
       <action selector="someMethod:" destination="2"
                eventType="touchUpInside" id="kdU-9Q-rW1"/>
    </connections>


2012年5月28日月曜日

Unknown type name 'MyClass'; did you mean 'SomeClass'?



Unknown type name 'MyClass'; did you mean 'SomeClass'?

クラス名が解決できない場合に出るメッセージですが、ヘッダファイルをimportしているのに発生することがあります。その場合は相互参照している可能性があります。

次のような場合に発生します。

#import "MyClass2.h"
@interface MyClass1
@property MyClass2 *myClass2;
@end

#import "MyClass1.h"
@interface MyClass2
@property MyClass1 *myClass1;
@end

この場合は一方のクラスでimportの代わりに@classを使うようにすると解決します。

@class MyClass2;
@interface MyClass1
@property MyClass2 *myClass2;
@end

#import "MyClass1.h"
@interface MyClass2
@property MyClass1 *myClass1;
@end

MyClass1のimplementationでMyClass2をimportする必要があります。

2012年5月22日火曜日

HTML5+JavaScriptによるSwipeの実装例

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

サンプルアプリで、iOSに似せたSwipeを実装してあります。Swipe画面でテーブルセルをSwipeすると「削除」ボタンが表示がされ、「削除」ボタンタップでテーブルセルが削除されます。
デスクトップではマウス操作で行うことができます。IEではレイアウトが崩れますが、一応Swipe動作します。

このサンプルアプリで使用しているのswipeに関するJavaScriptは次のとおりです。

//Table Cellなどでイベント発生
function swipeCellClicked(event) {
  var div = event.srcElement;
  //Swipeオブジェクトを作る
  new Swipe(div, event);
}
//イベントハンドラの設定などを行う。
Swipe = function(div, event) {
  this.start = new Date().getTime();
  this.div = div;
  //iOSの場合
  if (isIOS) {
    div.addEventListener("touchmove", Swipe.touchMove, false);
    div.addEventListener("touchend", Swipe.touchEnd, false);
  } else {
    //SafariなどaddEventListenerを使うブラウザの場合
    if (typeof div.addEventListener != "undefined") {
      div.addEventListener("mousemove", Swipe.touchMove, false);
      //div.addEventListener("mouseout", Swipe.touchEnd, false);
      div.addEventListener("mouseleave", Swipe.touchEnd, false);
      div.addEventListener("mouseup", Swipe.touchEnd, false);
    } else {
      //IEなどattachEventを使う場合
      div.attachEvent("onmousemove", Swipe.touchMove);
      div.attachEvent("onmouseout", Swipe.touchEnd);
      div.attachEvent("mouseup", Swipe.touchEnd);
    }
  }
  if (typeof event.pageX != "undefined") {
    Swipe.x0 = event.pageX;
    Swipe.y0 = event.pageY;
  } else {
    Swipe.x0 = event.x;
    Swipe.y0 = event.y;
  }
}
//touchMoveまたはmouseMoveイベント。イベント発生位置を保存。
Swipe.touchMove = function(event) {
  if (event != null) {
    if (typeof event.pageX != "undefined") {
      Swipe.x1 = event.pageX;
      Swipe.y1 = event.pageY;
    } else {
      Swipe.x1 = event.x;
      Swipe.y1 = event.y;
    }
  } else {
    Swipe.x0 = Swipe.y0 = Swipe.x1 = Swipe.y1 = 0;
  }
}
//イベント終了時
Swipe.touchEnd = function(event) {
  div = (event == null ? this : event.srcElement);
  //イベントハンドラ削除
  if (isIOS) {
    div.removeEventListener("touchstart", Swipe.touchMove);
    div.removeEventListener("touchmove", Swipe.touchMove);
    div.removeEventListener("touchend", Swipe.touchEnd);
  } else {
    if (typeof div.removeEventListener != "undefined") {
      div.removeEventListener("mousedown", Swipe.touchMove);
      div.removeEventListener("mousemove", Swipe.touchMove);
      div.removeEventListener("mouseout", Swipe.touchEnd);
      div.removeEventListener("mouseleave", Swipe.touchEnd);
      div.removeEventListener("mouseup", Swipe.touchEnd);
    } else {
      div.detachEvent("mousedown", Swipe.touchMove);
      div.detachEvent("onmousemove", Swipe.touchMove);
      div.detachEvent("onmouseout", Swipe.touchEnd);
      div.detachEvent("mouseup", Swipe.touchEnd);
    }
  }
  var dx = Swipe.x1 - Swipe.x0;
  var dy = Swipe.y1 - Swipe.y0;
  //上下移動があまりなく、左右移動が十分ある場合にSwipeとする。
  if ((dx < -50 || dx > 50) && dy > -50 && dy < 50) {
    Swipe.div = div;
    setTimeout(Swipe.appendButton, 10);
  }
  Swipe.x0 = Swipe.y0 = Swipe.x1 = Swipe.y1 = 0;
}

Swipe.div = null;
Swipe.prevSwipedDiv = null;
//Swipeされたときに「削除」ボタンを追加、あるいは既に追加されている場合は削除する。
Swipe.appendButton = function() {
  var div = Swipe.findCellBoxDiv(Swipe.div);
  if (Swipe.prevSwipedDiv != null && Swipe.prevSwipedDiv != div) {
    Swipe.removeButton(Swipe.prevSwipedDiv);
    Swipe.prevSwipedDiv = null;
    return;
  }
  if (Swipe.removeButton(div)) {
    Swipe.prevSwipedDiv = null;
    return;
  }
  Swipe.prevSwipedDiv = div;
  var btn = document.createElement("INPUT");
  btn.type = "button";
  btn.value = "削除";
  btn.className = "swipeDelete";
  if (typeof btn.addEventListener == "undefined") {
    btn.attachEvent("onclick", Swipe.deleteCell);
  } else {
    btn.addEventListener("click", Swipe.deleteCell, false);
  }
  div.appendChild(btn);
}
//既に「削除」ボタンがセットされていたらそれを削除する。
//子要素にINPUT要素があったら削除し、trueを返す。ない場はfalseを返す。
Swipe.removeButton = function(div) {
  if (div.childNodes) {
    for(var i=div.childNodes.length - 1; i>= 0; i--) {
      if (div.childNodes[i].nodeType == 1 && div.childNodes[i].tagName == "INPUT") {
        div.removeChild(div.childNodes[i]);
        return true;
      }
    }
  }
  return false;
}
//「削除」ボタンがタップされたテーブルセルを削除
Swipe.deleteCell = function() {
  var div = Swipe.findCellBoxDiv(event.srcElement);
  div.parentElement.removeChild(div);
  Swipe.prevSwipedDiv = null;
}
//イベントが発生した要素を含むテーブルセル(DIV)を探す。
Swipe.findCellBoxDiv = function(div) {
  var cbDiv = div;
  while(cbDiv != null) {
    if (cbDiv.className == "TableCellBox") {
      return cbDiv;
    }
    cbDiv = cbDiv.parentElement;
  }
  return null;
}

HTML5+JavaScriptによるSwipeの実装例

2012年5月16日水曜日

HTML5+JavaScriptで画面をスライドさせて切り替える方法


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

サンプルアプリではメイン画面のテーブル行を選択すると左にスライドして次の画面へ移動し、「戻る」ボタンでメイン画面に戻ります。

画面AからBへスライドして移動させる場合、次のような手順となります。
  • 同一HTML内にA、Bの両方のDIVを作る。
  • スライド前はAのみ表示する。
    画面固定にするために同時に表示される領域をデバイスのスクリーン内に収まるようにします。
  • スライド実行時にBをAの右側の位置に表示させる。
  • A、Bを同時に左方向にスライドさせる。
  • Aを非表示にする。
transformとtransitを用いて画面をスライドさせますが、アニメーションは非同期で行われるため、上記の手順を単純に記述したのでは一気に処理が終わり、アニメーション効果が出せません。そこでtimeoutを使用し、transit終了後に次の処理を開始するようにします。

サンプルアプリで画面スライドに関わる部分のJavaScriptは次のようになっています。

//グローバル変数
var mainBox = null;
var slideBox = null;

//body.onloadなどで初期化する。
function init() {
  mainBox = new SlideBox(divMainBox, null);
}

//テーブル行がタップされたとき呼ばれる。
function cellClicked(event) {
slideBox = new SlideBox(slideDiv, mainBox);
  slideBox.show();
}

//画面スライドを行うオブジェクト
SlideBox = function(div, prevBox) {
  this.div = div;
  this.div.className = "SlideBox";
  this.prevBox = prevBox;
}

SlideBox.sec = "0.3s";
SlideBox.msec = 300;
SlideBox.currBox = null;
SlideBox.divR = null;
SlideBox.divL = null;

//cellClickedから呼ばれ、画面スライドを開始する。
SlideBox.slide = function(moveX) {
  SlideBox.divL.style.webkitTransition = "all " + sec + " linear";
  SlideBox.divR.style.webkitTransition = "all " + sec + " linear";
  SlideBox.divL.style.webkitTransform = "translateX("+moveX+"px)";
  SlideBox.divR.style.webkitTransform = "translateX("+moveX+"px)";
}

SlideBox.hideBox = function(div1, div2) {
  div1.style.webkitTransition = "all 0s linear";
  div1.style.webkitTransform = "translateX(0px)";
  div2.style.display = "none";
}

SlideBox.prototype.show = function() {
  SlideBox.divR = this.div;
  SlideBox.divL = this.prevBox.div;
  SlideBox.divR.style.display = "block";
  setTimeout("SlideBox.postSlide()", 0);
}

SlideBox.postSlide = function() {
  SlideBox.slide(-320);
  setTimeout("SlideBox.postSlide2()", msec);
}

SlideBox.postSlide2 = function() {
  SlideBox.hideBox(SlideBox.divR, SlideBox.divL);
  return;
}

SlideBox.prototype.back = function() {
  SlideBox.divR = this.div;
  SlideBox.divL = this.prevBox.div;
  SlideBox.divL.style.display = "block";
  this.div.style.webkitTransform = "translateX(-320px)";
  setTimeout("SlideBox.postBack()", 0);
}

SlideBox.postBack = function() {
  SlideBox.slide(0);
  setTimeout("SlideBox.postBack2()", msec);
}

SlideBox.postBack2 = function() {
  SlideBox.hideBox(SlideBox.divL, SlideBox.divR);
  return;
}

HTML5+JavaScriptで画面をスライドさせて切り替える方法

2012年5月15日火曜日

HTML5+JavaScriptでiPhoneでスクロールしない画面を作る方法



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

HTML+JavaScriptで作った画面は、通常はiPhoneの画面サイズに合わせてリサイズされ、ピンチ操作で拡大すると上下左右にスクロールします。これをネイティブアプリらしくiPhone画面に合わせ、スクロールを上下のみとするか、無効にする方法です。
サンプルはこのURLに置いてあります。

iPhone(およびiPod touch)に合わせて作ってありますが、デスクトップのSafari、Chiromeでも動作の様子を確認できます。Androideでも動作するはずですが、確認していません。(動作報告頂けると幸いです。)

次の点がポイントです。

どちらにも共通の設定

画面の横サイズをデバイスサイズ以下にする

iPhoneの画面サイズはScale=1の場合にwidth=320px, height=480pxですが、heightは状況により異なります。
Scale=1以外の場合はScale後のサイズに合わせます。scale=2であればwidth=640px, height=960pxとなります。

ユーザーによるリサイズを無効にする

次のメタタグを設定します。
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
initial-scaleとmaximum-scaleを同じ値にすることでピンチされても画面の拡大、縮小が起こらなくなります。scaleの値は1以外でも構いません。

アドレスバーが表示されない位置に移動する

画面表示時に次のJavaScriptを実行します。
window.scrollTo(0,0);

これは画面固定とは直接関係ありませんが、 アドレスバーが隠れるとよりネイティブアプらしくなります。また、上下方向も固定にした場合は「戻る」ボタンが隠れたままになることがあるため、必ずこれを行うようにします。

画面固定の場合の設定

上下方向も固定にしたい場合はbodyのontouchmoveイベントハンドラで、このイベントを無効にします。これを行うと画面の大きさに関わらず画面スクロールしなくなります。
<body ontouchmove="event.preventDefault()">

この方法では全ての画面でスクロールが無効になります。特定の画面でだけスクロールを無効にしたい場合はフラグを使用して、フラグがtrueのときだけイベントを無効にすればよいでしょう。
<body ontouchmove="event.preventDefault();">

パノラマ写真をiPhoneやiPadを左右にパンして見るアプリです。
自分の周りをぐるりと回すと、臨場感のあるパノラマ写真閲覧ができます。
写真ライブラリの画像を見ることができ、自分で撮影したパノラマ写真を見ることができます。
iCloudにある画像も見ることができます。
視野角度を画像の縦横比較からアプリが設定します。
視野角度は60°〜360°の範囲で調整できます。
ontouchmoveはかなりな頻度で発生するので、パフォーマンスへの影響が気になる場合は必要なときだけイベントハンドラをadd/removeしてください。

ホーム画面にアイコンを置いた場合の画面サイズ調整

ホーム画面にアイコンを置いた場合は、次のメタタグにより全画面表示となり、アドレスバー、ステイタスバーが非表示となります。
<meta name="apple-mobile-web-app-capable" content="yes" />

この場合の画面サイズはステイタスバーのheight=20pxを除き、360px×460pxとなります。

ステイタスバーのスタイルを次のメタタグでblack-translucentとすると、ステイタスバーに隠れる部分も表示領域となり、360px×480pxとなります。
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />

これに合わせて画面レイアウトを調整したい場合は次の方法で画面サイズをチェックできます。
if (document.documentElement.clientHeight >= 460) {
//ホーム画面時のレイアウト設定
}

デスクトップとの共存

HTMLなのでデスクトップのブラウザでも表示可能です。デスクトップではなにもしなければ左寄せとなります。また右マージンでレイアウトしている場合は画面の幅の変動でレイアウトが崩れることがあります。デスクトップではセンタリングしたい、右マージンもそのまま有効にしたい、という場合はDIVのBOXを組み合わせることでiPhone、デスクトップの両方で同等の表示を行うことができます。
また、こうすることで画面サイズが異なるAndroid端末でもレイアウトが崩れないはずです(未確認)。

<body style="text-align:center;marigin:0px;padding:0px;">
    <div style="display:-webkit-box;width:320px;margin:0px auto 0px auto;padding:0px;">
        この中にHTMLを書く
    </div>
</body>