Hatena::Groupiphone-dev

iPhoneアプリ開発まっしぐら★ このページをアンテナに追加 RSSフィード

引っ越し後の日記はコチラです

tokoromのその他の日記

2010-10-02

UIScrollViewを絶対に斜めにスクロールさせない方法

| 14:52 | はてなブックマーク -  UIScrollViewを絶対に斜めにスクロールさせない方法 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UIScrollViewを絶対に斜めにスクロールさせない方法


UIScrollViewにはdirectionalLockEnabledというプロパティがあり、ここにYESを設定すると、上下にスクロールしはじめたら上下固定、左右にスクロールしはじめたら左右に固定となるため、斜めにスクロールできないようにしたい場合に便利です。

しかし、これには重大な欠陥(仕様?)があり、はじめから斜めにスクロールしはじめると斜めにスクロールできてしまうのです*1

そのため、斜めに絶対にスクロールさせないためには、自分で制御しなければなりません。


具体的には、以下のストーリーで制御を行ないます。

1. スクロール前のスクロール位置を保存しておく
2. スクロールしはじめたら、それが上下方向か左右方向かを判別して記憶する
3. 上下方向なら、それ以降のスクロールで左右に移動してしまったぶんを無理やり元に戻す
4. 左右方向なら、それ以降のスクロールで上下に移動してしまったぶんを無理やり元に戻す

スクロールの検知はUIScrollViewDelegateのscrollViewDidScroll:メソッド、スクロール位置の矯正はUIScrollViewのsetContentOffset:メソッド*2でそれぞれ行ないます。


具体的なコードは以下のとおり。

これをUIScrollViewを継承したクラスかUIScrollViewを管理しているクラスに追記します。

【interface側】

typedef enum
{
  kILScrollViewDirectionNone = 0,
  kILScrollViewDirectionHorizontal = 1,
  kILScrollViewDirectionVertical = 2,
} ILScrollViewDirection;

@property (nonatomic, assign) ILScrollViewDirection direction;
@property (nonatomic, assign) CGPoint beganPoint;

【implementation側】

@synthesize direction = direction_;
@synthesize beganPoint = beganPoint_;

// スクロール管理の初期化
- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
  self.direction = kILScrollViewDirectionNone;
  // スクロールしはじめのoffset管理
  self.beganPoint = [scrollView contentOffset];
}

// スクロール位置の矯正
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  CGPoint currentPoint = [scrollView contentOffset];
  if ( kILScrollViewDirectionNone == self.direction ) {
    // スクロール方向の決定
    if ( !CGPointEqualToPoint( currentPoint, self.beganPoint ) ) {
      CGFloat moveHorizontal = ABS( currentPoint.x - self.beganPoint.x );
      CGFloat moveVertical = ABS( currentPoint.y - self.beganPoint.y );
      if ( moveHorizontal < moveVertical ) {
        NSLog( @"direction = Vertical" );
        self.direction = kILScrollViewDirectionVertical;
      } else {
        NSLog( @"direction = Horizontal" );
        self.direction = kILScrollViewDirectionHorizontal;
      }
    }
  }
  if ( kILScrollViewDirectionVertical == self.direction ) {
      currentPoint.x = self.beganPoint.x;
      [scrollView setContentOffset:currentPoint];
  } else if ( kILScrollViewDirectionHorizontal == self.direction ) {
      currentPoint.y = self.beganPoint.y;
      [scrollView setContentOffset:currentPoint];
  }
}

この他、UIScrollViewDelegateには、

  • (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView;

で、慣性のアニメーションも含めてスクロールが完全に終わるのを検知する機能なども備わったりしています。

*1:iOS4.1現在

*2:contentOffsetプロパティ

DiosmaryDiosmary2012/02/07 02:19I feel saistfied after reading that one.

afkzorrxsliafkzorrxsli2012/02/07 17:36YwRFJD <a href="http://hfvnyjmaylup.com/">hfvnyjmaylup</a>

ryzvrizlxryzvrizlx2012/02/09 20:49YePVl1 , [url=http://ilafqvobogzb.com/]ilafqvobogzb[/url], [link=http://extouivsybdi.com/]extouivsybdi[/link], http://xivfvtxrgpua.com/

omjjiaxomjjiax2012/02/11 02:20GJQx3W <a href="http://vxlrpnigxovg.com/">vxlrpnigxovg</a>

czzqjeuczzqjeu2012/02/12 03:44rtKW1V , [url=http://dtzinzwejddp.com/]dtzinzwejddp[/url], [link=http://tabyjeeqfmpb.com/]tabyjeeqfmpb[/link], http://hpmxkcvmlqes.com/

CharlieCharlie2013/10/13 10:57Thinking like that is really imvisserpe

MarijanaMarijana2013/10/14 14:21You've hit the ball out the park! Inebcdirle!

KatrinaKatrina2013/10/15 04:02You <a href="http://ztpyupaomw.com">co'ulndt</a> pay me to ignore these posts!

StivenStiven2013/10/15 15:38That's a wise answer to a tricky quitseon http://jrqfcy.com [url=http://cvnkfkniwt.com]cvnkfkniwt[/url] [link=http://nlqktxnjnah.com]nlqktxnjnah[/link]

KahlouchaKahloucha2013/11/03 18:13Kewl you should come up with that. <a href="http://uacatmwe.com">Exltlcene!</a>

SluxzSluxz2013/11/12 23:30Check that off the list of things I was <a href="http://ynmgjzxubtd.com">coeunsfd</a> about.

ElenaElena2013/11/14 12:03At last, soenome who comes to the heart of it all http://hkvjrzl.com [url=http://vfqzwh.com]vfqzwh[/url] [link=http://wzkeisopfj.com]wzkeisopfj[/link]

HeberHeber2013/11/16 03:22That's a brilliant answer to an <a href="http://imsxwdeie.com">intsreeting</a> question

ToshihideToshihide2013/11/18 02:51I'm quite pleased with the inorfmation in this one. TY! http://pvoitao.com [url=http://ykoqymm.com]ykoqymm[/url] [link=http://ifhsrbz.com]ifhsrbz[/link]

2010-02-14

UITableViewで移動不可のセルを設定する方法の訂正

| 13:03 | はてなブックマーク -  UITableViewで移動不可のセルを設定する方法の訂正 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UITableViewで移動不可のセルを設定する方法の訂正


※Twitterで @pactii さんにご指摘いただいての訂正です。@pactiiさん、ありがとうございました!

UIKit本の 9.3.6 セルの移動 (P.301) で一番下のセルを移動不可にする方法として以下のコードを紹介しています。

- (BOOL)tableView:(UITableView*)tableView canMoveRowAtIndexPath:(NSIndexPath*)indexPath {
  // 最後のセル以外ならYES
  return ( dataSource_.count > indexPath.row + 1 );
}

これは、UITableViewDataSourceプロトコルのtableView:canMoveRowAtIndexPath:メソッドで、任意のrowに対する戻り値をNOとすることでそのrowの置き換えを制限するものです。これを利用することで、特定のセルが置き換えできないように指定することができます。9.3.6のサンプルコードでは、一番下の「新規追加」というセルを移動不可に設定しています(図.A)。


◆図.A

f:id:tokorom:20100214130121p:image


しかし、これだけだと、「新規追加」のセルを移動することはできなくても、他のセルを「新規追加」のセルの下に移動することができてしまうことが発覚しました。今回、この点を訂正しお詫びさせていただきます。

他のセルを「新規追加」の下に移動できなくするには、UITableViewDelegateのtableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:メソッドを使います。

- (NSIndexPath*)tableView:(UITableView*)tableView
    targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath*)sourceIndexPath
    toProposedIndexPath:(NSIndexPath*)proposedDestinationIndexPath
{
  if ( dataSource_.count > proposedDestinationIndexPath.row + 1 ) {
    return proposedDestinationIndexPath;
  } else {
    return sourceIndexPath;
  }
}

これは、セルの移動の挙動を細かく設定するためのメソッドで、

  • 移動前の場所がsourceIndexPath
  • 移動予定の場所がproposedDestinationIndexPath

にそれぞれ渡されてきます。

このメソッドの戻り値に指定した場所が実際の移動先になるため、特に挙動を変更しない場合はproposedDestinationIndexPathをそのまま返します。逆に言えば、移動を許可したくない場合にsourceIndexPathを返してやれば、移動がされなかったという扱いにすることが可能です。

今回で言えば、移動予定の場所(proposedDestinationIndexPath)が「新規追加」の場所よりも下だった場合にsourceIndexPathを返すようにすることで、「新規追加」の場所を常に一番下に保つことが可能になります。



全ての読者の皆様、至らない点があり申し訳ありませんでした。


UITableViewCellにコントロールを追加する方法の訂正

| 13:40 | はてなブックマーク -  UITableViewCellにコントロールを追加する方法の訂正 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UITableViewCellにコントロールを追加する方法の訂正


※Twitterで @ytka さんにご指摘いただいての訂正です。@ytkaさん、ありがとうございました!

UIKit本の 9.4.8 セルにコントロールを追加 (P.319) でセルのカスタマイズについて以下のようなコードを紹介しています。


■誤ったコード

- (UITableViewCell*)tableView:(UITableView*)tableView
  cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
  static NSString* identifier = @"basis-cell";
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if ( nil == cell ) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                  reuseIdentifier:identifier];
    [cell autorelease];
  }
  cell.textLabel.text =
    [[dataSource_ objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
  switch ( indexPath.section ) {
    case 0: //< セルにUIImageViewを追加
      [cell.contentView addSubview:[self imageViewForCell:cell withFileName:@"Samurai.png"]];
      break;
    case 1: //< セルにUISwitchを追加
      [cell.contentView addSubview:[self switchForCell:cell]];
      break;
    case 2: //< セルにUISliderを追加
      [cell.contentView addSubview:[self sliderForCell:cell]];
      break;
    default:
      break;
  }
  return cell;
}

これは、図.Bのようにセルに様々なコントロールを追加するためのサンプルコードです。

図.B

f:id:tokorom:20100214133638p:image


これは誤ったコードで、この例のようにセルの再利用が発生しにくいケースでこそ問題は顕在化しませんが、セルの再利用が発生したときに、その再利用されたセルに次々とコントロールがaddSubviewされて画面がぐちゃぐちゃになってしまいます。

このように、dequeueReusableCellWithIdentifier:メソッドで取得したセルに対してコントロールを追加することは基本的にやってはいけません。dequeueReusableCellWithIdentifier:メソッドで取得したセルに対しては、値の変更のみ行うのが正しいやりかたです。

修正したサンプルコードを以下に添付させていただきます。

ここではセルを作成する流れがわかる箇所のみ抜粋しています。全てのコードは、

に置いてありますので、全体が必要な場合にはお手数ですがこちらをご参照ください。

UIKit本をお持ちのかたは、Subversionから更新していただくことで、この修正を含めた全サンプルコードをダウンロードいただけます。

□修正後のサンプルコード

- (UITableViewCell*)tableView:(UITableView*)tableView
  cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
  // カスタムセルの種類によって再利用のためのIDを変えること
  static const id identifiers[3] = { @"image-cell", @"switch-cell", @"slider-cell" };
  NSString* identifier = identifiers[ indexPath.section ];
  UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier];
  if ( nil == cell ) {
    // セルにコントロールを追加する場合は、セルの作成時に行う
    // ここでは各カスタムセルごとにUITableViewCellのサブクラスを定義して使っている
    // 各サブクラスの実装は http://iphone-book-sample.googlecode.com/svn/trunk/Chapter9/TableSample/Classes/SampleForCustomizedCell.m 参照
    // なお、各initXXXメソッド内でそれぞれのセルに必要なコントロールをaddSubviewしている
    switch ( indexPath.section ) {
      case 0:
        cell = [[[CellWithImageView alloc] initWithReuseIdentifier:identifier] autorelease];
        break;
      case 1:
        cell = [[[CellWithSwitch alloc] initWithReuseIdentifier:identifier] autorelease];
        break;
      case 2:
      default:
        cell = [[[CellWithSlider alloc] initWithReuseIdentifier:identifier] autorelease];
        [(CellWithSlider*)cell setDelegate:self];
        break;
    }
  }
  // 再利用のときにも通るコードでは、各コントロールの値の変更のみ行う
  switch ( indexPath.section ) {
    case 0:
      [[cell imageView] setImage:[UIImage imageNamed:@"Samurai.png"]];
      break;
    case 2:
      {
        // UISlider付のセルなら値を設定
        NSNumber* value = [self.sliderValues objectAtIndex:indexPath.row];
        CellWithSlider* cellWithSlider = (CellWithSlider*)cell;
        cellWithSlider.slider.value = [value floatValue];
        cellWithSlider.row = indexPath.row;
      }
      break;
    default:
      break;
  }
  cell.textLabel.text =
    [[self.dataSource objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
  return cell;
}

全ての読者の皆様、至らない点があり申し訳ありませんでした。

notoroidnotoroid2010/03/29 18:12楽しく拝見させていただきました。ドロップ中に「新規追加」の場所を維持する場合、sourceIndexPath ではなく[NSIndexPath indexPathForRow:proposedDestinationIndexPath.row-1 inSection:proposedDestinationIndexPath.section] を返すのはいかがでしょう?

tokoromtokorom2010/03/29 22:21なるほど、そうすると(一番下の)新規追加のところに移動できなくても、そのすぐ上に移動することになるわけですね^^
名案ありがとうございます!

muriyarikayomuriyarikayo2011/08/03 19:36こんにちは。
画面に表示しきれない数のCellがある場合は、notoroidさんのおっしゃるように、
現在表示されているCellのindexPathを返さないと、非表示のCellのキャッシュが出来てしまいUIが崩壊します。

grxqcblnwpgrxqcblnwp2014/02/04 23:06fyixpjqipof.efw, <a href="http://www.djlxctovam.com/">slmmwekfjh</a> , [url=http://www.cjdqexhwco.com/]yvkvuhadyt[/url], http://www.rmwwdekfmq.com/ slmmwekfjh

2010-01-31

UITabBarControllerの"その他"画面と"配置変更"画面のbarStyleの変更

| 13:05 | はてなブックマーク -  UITabBarControllerの"その他"画面と"配置変更"画面のbarStyleの変更 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UITabBarControllerの"その他"と"配置変更画面"のbarStyleの変更


UITabBarControllerを利用する際に表示される「その他」画面に表示されるナビゲーションバーの色は、UITabBarControllerのnavigationBar.barStyleを変更しても変わることはない。

f:id:tokorom:20100131130209p:image

このナビゲーションバーの色を変更するには、以下のようにUITabBarControllerのmoreNavigationControllerプロパティのnavigationBar.barStyleを変更する必要がある。

// selfはUITabBarControllerのインスタンス
self.moreNavigationController.navigationBar.barStyle = UIBarStyleBlack;

これにより、その他画面のナビゲーションバーの色が変更される。

f:id:tokorom:20100131130330p:image

ただ、これで安心してはいけない。

これだけだと、その他画面の[編集]ボタンをタップしたときに遷移する配置変更画面のナビゲーションバーまでは変更されないのだ。

f:id:tokorom:20100131130331p:image

ここも変更したい場合、UITabBarControllerDelegateのtabBarController:willBeginCustomizingViewControllers:メソッドを以下のように実装する。

// tabBarControllerのdelegateにselfが設定されていること
// self自体がUITabBarControllerのインスタンスなら self.delegate = self;
- (void)tabBarController:(UITabBarController*)tabBarController
  willBeginCustomizingViewControllers:(NSArray*)viewControllers
{
  UIView* subviews = [tabBarController.view.subviews objectAtIndex:1];
  UINavigationBar* navigationBar = [[subviews subviews] objectAtIndex:0];
  navigationBar.barStyle = UIBarStyleBlack;
}

これで、その他画面と配置変更画面の両方のナビゲーションバーの色が変更されたはずだ。

f:id:tokorom:20100131130332p:image


■参考URL: http://blog.noizeramp.com/2009/08/06/iphone-black-navigation-bar-in-more-view/

2010-01-13

UIImageとNSDataの相互変換

| 04:47 | はてなブックマーク -  UIImageとNSDataの相互変換 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > UIImageとNSDataの相互変換


UIKit本の Unit5.4 でUIImageが登場するが、ここではUIImageのimageNamed:メソッドを使ってファイルからUIImageを作成する方法しか紹介できていない。

しかし他にも、NSURLConnectionを使ってWebからダウンロードした画像ファイルをUIImageとして使いたい場合など、NSDataからUIImageを作りたいことがよくあるだろう。

まず、これにはUIImageをinitWithData:メソッドで初期化すればよい。

// receivedDataをWebから取得した画像のバイナリデータ(NSData)とする
// ここではわかりやすいようにいったん代入する
NSData* data = receivedData;
// initWithData:メソッドでNSDataを元にUIImageを初期化できる
UIImage* image = [[[UIImage alloc] initWithData:data] autorelease];

また、逆にUIImageの画像データをNSDataとして抽出するにはUIImagePNGRepresentation関数が使える。

// UIImagePNGRepresentation関数によりUIImageが保持する画像データをPNG形式で抽出可能
NSData* pngData = [[[NSData alloc] initWithData:UIImagePNGRepresentation( image )] autorelease];

代わりにUIImageJPEGRepresentation関数を使ってJPEGで抽出することもできる。

SagiSagi2011/09/18 12:34Articles like this are an example of quick, heplufl answers.

bkkabtgkbkkabtgk2011/09/19 02:00awVhiI <a href="http://labdekfxzbvy.com/">labdekfxzbvy</a>

socuaxwkasocuaxwka2011/09/19 18:41t3SeSB , [url=http://wfrqwwtkkali.com/]wfrqwwtkkali[/url], [link=http://puasspigsryp.com/]puasspigsryp[/link], http://fhqvyfqalszm.com/

hkogvyhkogvy2011/09/24 01:18Mwa3mA <a href="http://lybzufdpfila.com/">lybzufdpfila</a>

amangvycozyamangvycozy2011/09/27 02:42Vqmgag , [url=http://wzvsychljbvd.com/]wzvsychljbvd[/url], [link=http://stfjlxfdlspx.com/]stfjlxfdlspx[/link], http://wahikvwfnhfk.com/

2010-01-10

UIKit本への追加コンテンツと補足情報をこちらに追加していきます

| 02:40 | はてなブックマーク -  UIKit本への追加コンテンツと補足情報をこちらに追加していきます - iPhoneアプリ開発まっしぐら★

◆UIKit本 追加コンテンツ&補足情報◆

f:id:tokorom:20100111023538j:image

ここには「iPhoneプログラミングUIKit詳解リファレンス」に載せきれなかった情報や、補足/訂正情報を掲載していきます。


  • UIKit本の内容自体への補足/訂正は → コチラ

  • 追加コンテンツ【Objective-C 2.0 コーディング解説編】 → コチラ

  • 追加コンテンツ【データ管理クラス解説編】 → コチラ

UIKit本の内容自体への補足/訂正

| 02:49 | はてなブックマーク -  UIKit本の内容自体への補足/訂正 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正



ここには「iPhoneプログラミングUIKit詳解リファレンス」の内容自体への補足/訂正情報を掲載していきます。


画面遷移時に呼ばれるメソッドの補足

| 04:07 | はてなブックマーク -  画面遷移時に呼ばれるメソッドの補足 - iPhoneアプリ開発まっしぐら★

UIKit本追加/補足 > UIKit本の内容自体への補足/訂正 > 画面遷移時に呼ばれるメソッドの補足


UIKit本の P.116 で、画面遷移時にUIViewControllerの状態監視用の各メソッドが、

  1. ■遷移先の画面のviewDidLoad(読み込み済みだった場合はこれを省く)
  2. ◇遷移元の画面のviewWillDisappear
  3. ■遷移先の画面のviewWillAppear
  4. ◇遷移元の画面のviewDidDisappear
  5. ■遷移先の画面のviewDidAppear

という順番でコールされると記載している。

f:id:tokorom:20100111043005p:image

しかし、これはUINavigationControllerのpushViewController:animated:メソッドによる画面遷移の場合の話だ。

これが、UIViewControllerのpresentModalViewController:animated:メソッドによる画面のモーダル表示の場合には少し変わってくる。

具体的には以下のとおりだ。

  1. ■遷移先の画面のviewDidLoad(読み込み済みだった場合はこれを省く)
  2. ■遷移先の画面のviewWillAppear
  3. ◇遷移元の画面のviewWillDisappear
  4. ■遷移先の画面のviewDidAppear
  5. ◇遷移元の画面のviewDidDisappear

このように、画面遷移時とモーダル表示時の場合にはこれらのメソッドがコールされるタイミングが変わってくるので注意が必要だ。

f:id:tokorom:20100111043006p:image

特に、

  1. 遷移元画面のviewWillDisappearで変数Aにnilを設定して
  2. 遷移先画面のviewWillAppearで変数Aにselfを設定する

というようなコードを書いて、変数Aにカレントの画面を常に代入するなんて処理になっているとまずいことが起こる。画面をモーダル表示する場合にはこれらのメソッドが呼ばれるタイミングが逆転するので、思いもよらず変数Aがnilになってしまうのだ。

いずれにせよ、場合によってviewWillAppear/viewWillDisappearの呼ばれる順番が変わってくる可能性があるということを頭の片隅に置いておいたほうが良さそうだ。

myumyu2011/04/20 09:25UIKit詳解リファレンスを見てアクセスしたのですが
追加コンテンツのリンクが切れているようです。
今後、公開予定はありますか?

tokoromtokorom2011/04/20 10:27リンク切れ失礼致しました。
サーバを移管した後、訂正をできておりませんでしたので、ただ今、修正させていただきましたので、お手数ですがもう一度ダウンロードをお願いします。
ご指摘ありがとうございます!

myumyu2011/04/20 11:56さっそく対応していただきありがとうございました。
助かります。

PandaPanda2011/05/15 08:19リファレンスを参考にさせていただいているのですが、
UINavigationControllerのpushViewControllerで2階層遷移後(A→B→C)、
Aまで戻るとき、B画面でViewWillDisappearが呼ばれません。
そういう仕様なのでしょうか?

StevenSteven2012/08/09 01:51Lot of smarts in that psontig!

dnhiyzdnhiyz2012/08/09 20:54ODUT7s , [url=http://fwvorbyxnpgq.com/]fwvorbyxnpgq[/url], [link=http://acrfzafvzhaa.com/]acrfzafvzhaa[/link], http://jrjcdxrbuhey.com/

kkpfvtskkpfvts2012/08/10 04:09BxhGor <a href="http://tktagzmmdeys.com/">tktagzmmdeys</a>

xsifaapxtxsifaapxt2012/08/12 10:20kl1fBF , [url=http://nobcivkucvaj.com/]nobcivkucvaj[/url], [link=http://rnllvizmreux.com/]rnllvizmreux[/link], http://kwopsaywcwpo.com/

a_ishida_mma_ishida_mm2014/07/30 19:47お忙しいところ失礼します。
本日tokorom様UIKit詳細リファレンスの本を購入したのですが、
参考のドキュメントのURLから資料がダウンロードできない状況です。
http://data.main-loop.com/iphone_docs/chapter01.pdf
http://data.main-loop.com/iphone_docs/chapter02.pdf
お手数ですが、ご案内いただくこと可能でしょうか。
何卒、よろしくお願いいたします。