Hatena::ブログ(Diary)

中継地点 RSSフィード

2012-11-02

UITableViewでラウンドロビン的な無限スクロールCoverFlowを作る

「UITableViewでCoverFlowを作れるか?」というネタに興味が湧いたのでちょっとやってみました。

UITableViewハック

ちなみに、iOSのCoverFlowについて調べてみると下記のようなframeworkが見つかりました。

ソースを覗くと、TapkuがUIScrollView、その他はUIViewでゴリゴリ作っているみたいです。

UITableViewで作る最大のメリットはCellの再利用機構にあり、UITableViewDataSourceとUITableViewDelegateをそのまま活用できます。今回、折角のUITableViewなので無限にスクロールできるラウンドロビン的なCoverFlowを試してみました。

結果、こんな感じのものができました。

D


色々と使えそうなテクニックもあると思いますので作り方を紹介します。


無限スクロールTableViewの作り方

あらかじめ余分のDataSourceを用意し限界位置までスクロールされたらcontentOffsetを設定し直すという方法で行います。具体的にはDataSourceを4倍分のDataSourceを作成しscrollViewDidScroll:でcontentOffsetをチェックします。contentSize.heightの1/8に到達したら5/8に、6/8に到達したら2/8にcontentOffsetを設定しなおすという具合です。

f:id:h_mori:20121102095504p:image:w360


- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self moveCenterOfContent:scrollView];
}

- (void)moveCenterOfContent:(UIScrollView *)scrollView {
    CGFloat currentOffsetX = scrollView.contentOffset.x;
    CGFloat currentOffSetY = scrollView.contentOffset.y;
    CGFloat contentHeight = scrollView.contentSize.height;
   
    if (currentOffSetY < contentHeight/8) {
        scrollView.contentOffset = CGPointMake(currentOffsetX, currentOffSetY + contentHeight/2);
    }
    if (currentOffSetY > contentHeight*6/8) {
        scrollView.contentOffset = CGPointMake(currentOffsetX, currentOffSetY - contentHeight/2);
    }
}

DataSourceがTableViewサイズより小さい場合(初期表示時にスクロールしない場合)は、初期処理でTableViewサイズを超えるまでDataSourceを等倍し、それを1セットとして4倍して作るようにします。

- (void)fillRecordsForView {
    CGFloat currentOffSetY = self.tableView.contentOffset.y;
    CGFloat contentHeight = self.tableView.contentSize.height;
    CGFloat viewHieght = self.tableView.bounds.size.height;
   
    if (currentOffSetY >= contentHeight - viewHieght) {
        [_records addObjectsFromArray:_records];
        [self.tableView reloadData];
       
        //画面サイズを超えるまで繰り返す
        [self fillRecordsForView];
    }
}

UIScrollViewDelegateを使っているのを見れば分かる通り、このテクニックはUIScrollViewで利用されるものです。



UITableViewのカスタマイズ


設計

カスタマイズしやすくするため、UITableViewControllerは使わずUIViewController.view上にUITableViewを配置し、UITableViewCellの上にUIImageViewを配置します。

それぞれのコンポーネントに役割を持たせたいのでUIKitを拡張してsubclass化します。


  • CFCoverFlowViewController : UIViewController
  • CFCoverFlowView : UITableView
    • CoverFlowのデータ管理
    • layoutSubviewsの実装
  • CFCoverFlowCell : UITableViewCell
    • TableView中央との距離の管理
    • CellのCATransform3D処理
    • スケール、位置、アングル、影の係数算出
  • CFCoverFlowImageView : UIImageView
    • 反射、影などのsublayerの管理

UITableViewのAffine変換

CFCoverFlowViewの初期化処理で左90度のCGAffineTransform変換をかけます。


CFCoverFlowView.m

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        //左に90度回転
        CGAffineTransform t = CGAffineTransformMakeRotation(-M_PI/2.0f);
        self.transform = t;
    }
    return self;
}

UITableViewCellのCATransform3D処理

変形処理を開始するタイミングは CFCoverFlowViewController#scrollViewDidScroll (スクロール中) と

CFCoverFlowView#layoutSubviews: (初期表示時)に行います。


CFCoverFlowViewController.m

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self moveCenterOfContent:scrollView];
    // 変換処理
    [(CFCoverFlowView *)scrollView transformView];
}

CFCoverFlowView.m

- (void)layoutSubviews {
    [super layoutSubviews];
    // 変換処理
    [self transformView];
}

変形対象のCellは現在表示されている部分だけに限定して、UITableView#indexPathsForVisibleRowsを使います。

尚、注意点としてUITableViewのCellは再利用されるため、tableView.subviewsはバラバラとなっており、そのまま変形すると遠近感が残念なことになってしまうので、予めソートして配置順を並び替える必要があります。


CFCoverFlowView.m

- (void)transformView {
    NSArray *sortedCells = [self sortedVisibleCellsByDistance];
   
    for (CFCoverFlowCell *cell in sortedCells) {
        //順番に背面から最前面に配置
        [self bringSubviewToFront:cell];
        //Cellの変形処理
        [cell transformCell];
    }
}

- (NSArray *)sortedVisibleCellsByDistance {
    NSMutableArray *sortedCells = [NSMutableArray array];
   
    NSArray *indexPaths = [self indexPathsForVisibleRows];
    for (NSIndexPath *indexPath in indexPaths) {
        CFCoverFlowCell *cell = (CFCoverFlowCell *)[self cellForRowAtIndexPath:indexPath];
        cell.distance = [self distanceFromCenter:indexPath];
        [sortedCells addObject:cell];
    }
    return [sortedCells sortedArrayUsingSelector:@selector(compare:)];
}

- (CGFloat)distanceFromCenter:(NSIndexPath *)indexPath {
    CGRect rect = [self rectForRowAtIndexPath:indexPath];
    CGFloat centerCellOffset = rect.origin.y+rect.size.height/2;
    CGFloat offset = self.contentOffset.y;
    CGFloat centerOffset = (self.bounds.size.height/2)+offset;
    return (centerCellOffset - centerOffset);
}

CFCoverFlowCell.m

- (NSComparisonResult)compare:(CFCoverFlowCell *)cell {
    if (fabs(self.distance) > fabs(cell.distance)) {
        return NSOrderedAscending;
    } else if (fabs(self.distance) < fabs(cell.distance)) {
        return NSOrderedDescending;
    } else {
        return NSOrderedSame;
    } 
}


Cellの変形処理

ここで視点Z座標の指定、右90度回転、角度調整、スケール調整、位置調整、影の透明度調整を行います。

尚、ここでもUITableViewのCellは再利用されるため、view.layer.positionを毎回初期化してあげる必要がります。

変形パラメータについて、画面の相対位置に対して変形係数を計算する処理を関数化しておくと色々と試したり複合的に使用する場合にメンテナンスがしやすくなります。


CFCoverFlowCell.m

- (void)transformCell {
    static const float zDistance = 800.0f;
   
    //CATransform3D前に毎回positionを設定し直す
    self.coverFlowImageView.layer.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2);

    float rate = [self rateOfDistance];
   
    CATransform3D t = CATransform3DIdentity;
    //視点の距離
    t.m34 = 1.0f / -zDistance;
    //右に90度回転
    t = CATransform3DRotate(t,
                            M_PI/2.0f,
                            0.0f,
                            0.0f,
                            1.0f);
    //角度
    t = CATransform3DRotate(t,
                            [CFCoverFlowCell angleForDistanceRate:rate],
                            0.0f,
                            1.0f,
                            0.0f);
    //スケール
    t = CATransform3DScale(t,
                           [CFCoverFlowCell scaleForDistanceRate:rate],
                           [CFCoverFlowCell scaleForDistanceRate:rate],
                           [CFCoverFlowCell scaleForDistanceRate:rate]);
    //位置
    t = CATransform3DTranslate(t,
                               [CFCoverFlowCell translateForDistanceRate:rate],
                               0.0f,
                               0.0f);
    self.coverFlowImageView.layer.transform = t;
   
    //影の透明度
    self.coverFlowImageView.shadowLayer.opacity = [CFCoverFlowCell shadowOpacityForDistanceRate:rate];
}

UITableViewの中央を0、左端を-1、右端を1としてUITableView中央からCell中央までの距離を相対値で算出します。

f:id:h_mori:20121102102404p:image:w240


CFCoverFlowCell.m

- (float)rateOfDistance {
    return (float)(self.distance * 2.0f / self.frame.size.width);
}

CoverFlowの動作を想像しながら、それぞれの変形係数について相対値に対した出力値をグラフ化しておきます。特に理由がない限り連続な線になるようにしてください。

最初は複雑な曲線は描かず、単純な区分線形関数で表せる程度のものにして挙動を確認しながら複雑な非線形曲線に調整していくとよいと思います。

f:id:h_mori:20121102102405p:image:w480

f:id:h_mori:20121102102406p:image:w480

f:id:h_mori:20121102102407p:image:w480

f:id:h_mori:20121102102408p:image:w480


グラフを元にそれぞれの関数の計算式を組みます。

CFCoverFlowCell.m

+ (float)scaleForDistanceRate:(float)rate {
    static const float coefficient = 10.0f;
    static const float maxScale = 2.0f;
    static const float minScale = 1.0f;

    if (fabsf(rate) > 0.1f) {
        return minScale;
    }
    return - (coefficient * fabs(rate)) + maxScale;
}

+ (float)angleForDistanceRate:(float)rate {
    static const float coefficient = 4.0f;
    static const float baseAngle = - M_PI/3.0f; //60 degree
   
    if (fabsf(rate) > 0.25f) {
        return copysignf(1.0f, rate) * baseAngle;
    }
    return coefficient * rate * baseAngle;
}

+ (float)translateForDistanceRate:(float)rate {
    static const float coefficient = 200.0f;

    if (fabs(rate) < 0.25f) {
        return coefficient * rate;
    }
    return - (coefficient * rate) + (copysignf(1.0f, rate) * coefficient/2.0f);
}

+ (float)shadowOpacityForDistanceRate:(float)rate {
    static const float coefficient = 1.0f;

    if (fabs(rate) < 0.1f) {
        return 0.0f;
    }
    return coefficient * fabsf(rate) - (coefficient * 0.1f);
}

ImaveViewのエフェクト

init処理にて反射のlayer、反射の影のlayer、ImageView・反射layerに対する相対値に応じた影のlayerを配置する。それぞれのframeは可変なのでframeの設定はlayoutSublayersOfLayer:で行います。

尚、ここでもUITableViewCell再利用のために反射のlayerにゴミが残ってしまうので反射layerのcontents設定はImageView本体のsetImageをオーバーライドしてこのタイミングで設定するようにします。


CFCoverFlowImageView.m

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self addLayer];
    }
    return self;
}

- (void)addLayer {
    static float const reflectionShadowOpacity = 0.7f;
   
    _shadowLayer = [CALayer layer];
    _shadowLayer.backgroundColor = [[UIColor blackColor] CGColor];
     _shadowLayer.masksToBounds = YES;
    _shadowLayer.opacity = 1.0f;

    _reflectionLayer = [CALayer layer];
     _reflectionLayer.masksToBounds = YES;
    _reflectionLayer.opacity = 1.0f;

    _reflectionLayer.transform = CATransform3DMakeScale(1.0f, -1.0f, 1.0f);
    _reflectionLayer.contentsGravity = kCAGravityResize;
    _reflectionLayer.sublayerTransform = _reflectionLayer.transform;
   
    _reflectionShadowLayer = [CALayer layer];
    _reflectionShadowLayer.backgroundColor = [[UIColor blackColor] CGColor];
     _reflectionShadowLayer.masksToBounds = YES;
    _reflectionShadowLayer.opacity = reflectionShadowOpacity;
   
    [_reflectionLayer addSublayer:_reflectionShadowLayer];
    [self.layer addSublayer:_reflectionLayer];
    [self.layer addSublayer:_shadowLayer];
}

- (void)layoutSublayersOfLayer:(CALayer *)layer {
    static float const space = 3.0f;
   
    CGRect shadowRect = layer.bounds;
    shadowRect.size.height = (layer.bounds.size.height * 2) + space;
    _shadowLayer.frame = shadowRect;
   
    CGRect reflectionRect = layer.bounds;
    reflectionRect.origin.y = layer.bounds.size.height + space;
    _reflectionLayer.frame = reflectionRect;
   
    _reflectionShadowLayer.frame = layer.bounds;

    [super layoutSublayersOfLayer:layer];
}


- (void)setImage:(UIImage *)image {
    [super setImage:image];
    _reflectionLayer.contents = self.layer.contents;
}

スクロール停止時の調整処理

スクロール停止時に中央に近いCellをTableView中央にフィットさせます。ハンドリングはドラッグ停止 (scrollViewDidEndDragging:)とスクロール慣性停止 (scrollViewDidEndDecelerating:)で行います。


CFCoverFlowViewController.m

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if(!decelerate) {
        [(CFCoverFlowView *)scrollView fitCenterForCell];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [(CFCoverFlowView *)scrollView fitCenterForCell];
}

CFCoverFlowView.m

- (void)fitCenterForCell {
    NSArray *sortedCells = [self sortedVisibleCellsByDistance];
    if (sortedCells.count > 0) {
        CFCoverFlowCell *cell = [sortedCells lastObject];
        [self setContentOffset:CGPointMake(self.contentOffset.x, self.contentOffset.y+cell.distance) animated:YES];
    }
}

その他の調整

CoverFlowはcontentの起点がTableView中央に位置するため、初期表示時とデバイス回転時に調整が必要です。

初期表示時のcontentOffsetはtableView.contentSize.heightを元に計算しますが、DataSourceを作成、tableView#reloadDataの実行後である必要があります。考えてみれば当たり前なんですが暫く悩みました。

デバイス回転をサポートしている場合は回転時にcontentOffsetの調整が必要になります。いくつか方法があるとは思いますがサンプルコードではデバイスサイズの縦横長の差分の半分を調整しています。



まとめ

UITableViewのCellは再利用されるため、Cellの値の初期化やsubviewの順番やガベージデータ、frame設定のタイミングなどで、いくつかの注意点が必要ですがそれさえ気をつければかなり楽に作ることができます。

この方法を使えば、CoverFlowだけでなく面白いエフェクトが作れそうな気がします。

カスタマイズのポイントとしては

  • UITableViewを左90度回転
  • Cellのlayerに対してCATransform3D変形をかける
    • 右90度回転
    • スケール、位置、アングル、影、反射など
    • TableViewに対するCellの相対位置を計測し、変形係数を算出
    • 変形タイミングはスクロール時、TableView初期表示時に行う
  • CellのViewの重なり順の並び替え
    • TableViewの中央に遠い順にソートしCellを重ね合わせる(bringSubviewToFront:)
  • スクロール停止時に近いCellを中央にフィットさせる
  • 初期表示時のcontentOffsetの調整
  • デバイス向き変更時のcontentOffsetの調整


サンプルコード

今回作ったサンプルプロジェクトをgithubにアップしました。

https://github.com/hmori/CoverFlowTest

2012-10-17

サーバから自由にカスタマイズ可能なメニューを実装する

メニューの項目や階層構造をアプリのリリース後に自由に変更できるようにしたいことがあります。

基幹系アプリの場合によくある理由として

  • 常時通信環境で運用するわけではない
  • 運用してみないとよりよいメニュー構造がわからない
  • iOS技術者ではない運用者がメニュー階層を簡単に弄りたい
  • WebViewでは野暮ったい などなど

確かにメニュー階層はアプリ使い勝手を左右する重要な部分なのでよく納得できる内容です。


今回、メニュー構成(plist)をWeb上に配置して自由にカスタマイズできるような実装を行なってみました。要件としては下記のような感じです。

若干変態的なのですがメニューはUITableViewを動的な作りにして実装します。


Menu.plistの構造

メニューを構成する必要な情報は全てMenu.plistに保持するように定義します。

  • Root (Dictionary)
    • Node (Dictionary) ※ルート画面の情報
      • Title (String) ※タイトルバー
      • Table (Array)
        • Item (Dictionary) ※1セクションの情報
          • SectionTitle (String) ※セクションの表示文字
          • SectionFooter (String) ※フッターの表示文字
          • Rows (Array)
            • Item (Dictionary) ※1セルの情報
              • CellIdentifier (String) ※セルのID、CellのStyleを定義
              • Text (String) ※セルの表示文字
              • DetailText (String) ※セル詳細の表示文字
              • Height (Number) ※セルの高さ
              • SegueIdentifier (String) ※セル選択時の起動SegueID、遷移先を定義
                • Node (Dictionary) ※次画面の情報 (以下繰返)

メニューアップデート時の通信を1トランザクションで終わらせるために遷移先の画面情報もMenu.plistの1ファイルで保持するようにしました。

サンプル

f:id:h_mori:20121017020151p:image:w640



Storyboardの定義

ここではStoryboardの名前をMain (Main.storyboard)とします。起動時のInfo.plistのstoryboard名をMainに定義します。

f:id:h_mori:20121017020153p:image:w640


この際、AppDelegateのdidFinishLaunchingWithOptionsではwindowの初期化を行わないようにします。

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    return YES;
}

MenuViewContollerと遷移されるLeaf(葉)となるViewContollerを全て定義し、LeafへのSegueはmanual定義するようにしておきます。

f:id:h_mori:20121017020154p:image:w640


Segueのmanual定義は、MenuViewControllerのバー部分を右クリックしてTriggered Segueのmanualから引っ張ります。またSegueを起動するためIDを振っておきます。

f:id:h_mori:20121017021236p:image:w640

f:id:h_mori:20121017021235p:image:w640


manual定義した場合、条件によりUIViewController#performSegueWithIdentifier:sender: で分岐させることが可能となります。


MenuViewControllerにUITableViewCellをStyle分用意してそれぞれIDを振ります。(Basic、RightDetail、LeftDetail、Subtitle)

f:id:h_mori:20121017021237p:image:w640


Segueは遷移処理(アニメーション等)を定義するものですが、遷移先(Leaf)に対して1本用意してIDによりSegueを呼び出すようにします。多段階層メニューなのでMenuViewControllerをループさせるmanual定義はStoryboardから行えないので、ソース内で手動で処理します。



MenuViewControllerのTableViewの実装

Node(1画面の表示情報)をNSDictionaryで保持し、各UITableViewのDataSource、Delegateを実装します。


FMMenuViewController.h

@interface FMMenuViewController : UITableViewController {
    NSDictionary *_node;
}
@property (strong, nonatomic) NSDictionary *node;
@end

FMMenuViewController.m DataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self tables] count];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return [[self sectionItem:section] objectForKey:kSectionTitle];
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
    return [[self sectionItem:section] objectForKey:kSectionFooter];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [[self rows:section] count];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat height = [[[self rowItem:indexPath] objectForKey:kHeight] floatValue];
    if (height <= 0) {
        return 44.0f; //default height
    }
    return height;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary *rowItem = [self rowItem:indexPath];
    NSString *cellIdentifier = [rowItem objectForKey:kCellIdentifier];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
    cell.textLabel.text = [rowItem objectForKey:kText];
    cell.detailTextLabel.text = [rowItem objectForKey:kDetailText];
    
    NSString *segueIdentifier = [rowItem objectForKey:kSegueIdentifier];
    if (segueIdentifier.length > 0) {
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    } else {
        cell.accessoryType = UITableViewCellAccessoryNone;
    }
    return cell;
}

#pragma mark - Private methods
- (NSArray *)tables {
    return [_node objectForKey:kTable];
}

- (NSDictionary *)sectionItem:(NSInteger)section {
    return [[self tables] objectAtIndex:section];
}

- (NSArray *)rows:(NSInteger)section {
    return [[[self tables] objectAtIndex:section] objectForKey:kRows];
}

- (NSDictionary *)rowItem:(NSIndexPath *)indexPath {
    return [[self rows:indexPath.section] objectAtIndex:indexPath.row];
}

特定のCell情報、Rows->ItemにsegueIdentifierが定義されていない場合は、アクセサリにDisclosure Indicator (青の右矢印)をつけないようにしています。



FMMenuViewController.m Delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSDictionary *rowItem = [self rowItem:indexPath];
    NSString *segueIdentifier = [rowItem objectForKey:kSegueIdentifier];
    
    if (segueIdentifier.length > 0) {
        if ([segueIdentifier isEqualToString:kMenuSegue]) {
            UIStoryboard *storyboard = [UIStoryboard storyboardWithName:kStoryboardIdentifier bundle:nil];
            FMMenuViewController *ctl = [storyboard instantiateViewControllerWithIdentifier:kMenuControllerIdentifier];
            ctl.node = [rowItem objectForKey:kNode];
            [self.navigationController pushViewController:ctl animated:YES];
        } else {
            @try {
                [self performSegueWithIdentifier:segueIdentifier sender:rowItem];
            }
            @catch (NSException *exception) {
                NSLog(@"NSException : performSegueWithIdentifier : %@", exception);
            }
        }
    }
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}


前述で書いた通り、manualの再帰的なSegueはStoryboardで定義できないので、手動でnavigationControllerにpushします。具体的にSegueIdentifierが"menuSegue"の場合にStoryboardからFMMenuViewControllerをインスタンス化し、次画面用のNode情報を設定してpushします。

            UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
            FMMenuViewController *ctl = [storyboard instantiateViewControllerWithIdentifier:@"menu"];
            ctl.node = [rowItem objectForKey:kNode];
            [self.navigationController pushViewController:ctl animated:YES];

この際、FMMenuViewControllerのStoryboard IDが必要になるので、StoryboardでIDを"menu"と振っておきます。

f:id:h_mori:20121017021238p:image:w640



対象のセルを選択時、Menu.plistのSegueIdentifierで定義したIDを読込んでStoryboardのLeafのSegueを呼び出します。

この時にSegueIdentifierがStoryboard上に存在しない場合、例外が発生しますのでcatchするようにします。

            @try {
                [self performSegueWithIdentifier:segueIdentifier sender:rowItem];
            }
            @catch (NSException *exception) {
                NSLog(@"NSException : performSegueWithIdentifier : %@", exception);
            }

UIViewController#prepareForSegue:sender: は performSegueWithIdentifier:でSegueが起動する直前に呼び出されます。prepareForSegueではLeafへのパラメータを渡しの処理を記述します。prepareForSegueのsenderはperformSegueWithIdentifier:発行時のsenderが渡されます。ここではrowItemが渡されます。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    FMViewController *viewController = segue.destinationViewController;
    [viewController setNode:[sender objectForKey:kNode]];
}

FMViewController.h

@interface FMViewController : UIViewController {
    NSDictionary *_node;
}
@property (strong, nonatomic) NSDictionary *node;
@end

FMViewController.m

@implementation FMViewController
@synthesize node = _node;

- (void)viewDidLoad {
    [super viewDidLoad];
    if (self.node) {
        self.title = [self.node objectForKey:kTitle];
    }
}

FMViewControllerのような全Leafの基底クラスを準備しすると、FMMenuViewControllerと各LeafのControllerは疎結合とすることができます。



Menu.plistの読込処理

FMMenuViewControllerのnodeがnilの場合は現画面がルートメニューであると判断します。viewDidLoadでローカルのDocumentディレクトリにMenu.plistが存在するかチェックし、存在すればNSDictionary#dictionaryWithContentsOfFile: で読み込みます。

    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filepath = [docPath stringByAppendingPathComponent:@"Menu.plist"]
    if ([[NSFileManagerdefaultManager] fileExistsAtPath:filepath]) {
        //load local file
        self.node = [[NSDictionarydictionaryWithContentsOfFile:filepath] objectForKey:kNode];
    }

Menu.plistが存在しなければサーバにリクエストを発行します。NSDictionary#dictionaryWithContentsOfURL:でも簡単に読込できるのですが、このメソッドは同期処理であることとタイムアウトが検知できないため避けるべきだと思います。(※通信中はフリーズしてしまう)

iOS5からNSURLConnection#sendAsynchronousRequest:queue:completionHandler:という簡単に非同期処理が行えますのでこちらを使います。Menu.plistのパースにはiOS5から標準で追加されたNSJSONSerializationが便利そうなのでこちらを利用します。

- (void)requestData {
    
    NSURL *url = [NSURL URLWithString:kRequestUrl];
    NSURLRequest *request = [NSURLRequest requestWithURL:url
                                             cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                         timeoutInterval:10.0f]; //timeout 10 seconds
    
    //indicator start
    [self.indicator startAnimating];
    
    __block FMMenuViewController *weakSelf = self;
    [NSURLConnection
     sendAsynchronousRequest:request
     queue:[[NSOperationQueue alloc] init]
     completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
         //indicator stop
         [weakSelf.indicator performSelectorOnMainThread:@selector(stopAnimating)
                                              withObject:nil
                                           waitUntilDone:YES];
         if (!error) {
             //parse JSON format
             NSDictionary *root = [NSJSONSerialization JSONObjectWithData:data
                                                                  options:NSJSONReadingAllowFragments
                                                                    error:nil];
             //write local file
             [root writeToFile:[weakSelf documentFilepath] atomically:YES];
             
             weakSelf.node = [root objectForKey:kNode];
             [weakSelf performSelectorOnMainThread:@selector(refreshData:)
                                        withObject:nil
                                     waitUntilDone:YES];
         }
     }];
}

- (void)refreshData:(id)sender {
    self.title = [_node objectForKey:kTitle];
    [self.tableView reloadData];
}

読み込んだパース済みのrootはNSDictorary#writeToFile:atomicallyで次回用にローカルファイルに書き込みます。

completionHandler:ブロックは応答があった時の処理ですが、ここは非同期スレッドで動作するのでUI処理はメインスレッドに同期させるようにします。また、循環参照リークを引き起こす可能性があるのでブロック内部から外部の変数へ直接アクセスしないように注意します。


Xcodeで作成したplistはxmlフォーマットなのですが、MacOSにはplutilコマンドが用意されており、xml <-> jsonの変換は可能です。

http://azu.github.com/2012/09/18/plist-to-json-to-objective-c-literal/


Menu.plistをMenu.jsonに変換

$ plutil -convert json -o Menu.json Menu.plist

XMLReader

参考までに、JSONじゃなくてXMLをそのまま扱いたい場合は、サードパーティライブラリでXMLReaderというものが便利に使えそうです。MIT Licenseで提供されています。(※著作権表示および許諾表示義務のみ)

https://github.com/Insert-Witty-Name/XML-to-NSDictionary

参考)http://www.zero4racer.com/blog/371



サンプルコード

サンプルコードをgithubにアップしました。

https://github.com/hmori/FlexibleMenuSample

f:id:h_mori:20121017021239p:image:w240

f:id:h_mori:20121017021240p:image:w240

f:id:h_mori:20121017021241p:image:w240

このサンプルコードはソース上に固有の情報を持たないシンプルな作りになっています。梱包されているMenu.plistは構造のサンプルで、実際にはソース上から参照していません。動作するMenu.plistはFMMenuViewController.mのkRequestUrlのjsonファイルになります。


この方法を使えばAppStore審査通過後に隠し機能を有効にするというイースターエッグが埋め込めますが、ディベロッパー規約違反になりますのでご注意を。

2012-10-11

iOS6のSocial.frameworkを試してみた

iOS6からFacebook、微博(Weibo)、Twitterを統合したSocial.frameworkが追加されました。例によってAOuth認証などの面倒な手続きは全てiOS側で行なってくれるので投稿だけであれば、数行のコードで実装が可能です。

iOS5から追加されたTwitter.frameworkは廃止の方向になるので、新しく実装する場合はSocial.frameworkで実装した方がよさそうです。実装方法はTwitter.frameworkと同じような作りとなっているので簡単に移行できると思います。


SLComposeViewControllerでの投稿方法

プロジェクトのBuild PhasesのLink Binary with Librariesで「Social.framework」を追加します。

f:id:h_mori:20121009121822p:image:w360

ヘッダにSocial.hをインポートします。

#import <Social/Social.h>

投稿するアクションにてSLComposeViewControllerを作成し、presentViewController で呼び出します。

    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) { //利用可能チェック
        NSString *serviceType = SLServiceTypeTwitter;
        SLComposeViewController *composeCtl = [SLComposeViewController composeViewControllerForServiceType:serviceType];
        [composeCtl setCompletionHandler:^(SLComposeViewControllerResult result) {
            if (result == SLComposeViewControllerResultDone) {
                //投稿成功時の処理
            }
        }];
        [self presentViewController:composeCtl animated:YES completion:nil];
    }

serviceTypeはSLServiceTypeTwitter、SLServiceTypeFacebook、SLServiceTypeSinaWeiboが用意されており、それぞれに投稿画面が用意されています。

f:id:h_mori:20121009121823p:image:w240

f:id:h_mori:20121009121824p:image:w240

f:id:h_mori:20121009121825p:image:w240




標準の投稿画面を使用せずにカスタムビュー等で直接投稿する方法

SLRequestを使用することでSLComposeViewControllerを使わずに直接それぞれのWebAPIを叩くことができます。ACAccountStoreでiOSのアカウント認証情報がラップされておりOAuth認証周りをSLRequestが吸収してくれるのでWebAPIのみに専念できます。

プロジェクトのBuild PhasesのLink Binary with Librariesで「Accounts.framework」を追加します。

f:id:h_mori:20121010230643p:image:w360

ヘッダにAccounts.hをインポートします。

#import <Accounts/Accounts.h>


Twitterの場合

投稿だけならTwitterの開発者登録(appkey等)は不要です。iOSのアカウント認証ACAccountが全て上手くやってくれます。

    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeTwitter]) {
        ACAccountStore *accountStore = [[ACAccountStore alloc] init];
        ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
        [accountStore
         requestAccessToAccountsWithType:accountType
         options:nil
         completion:^(BOOL granted, NSError *error) {
             if (granted) {
                 NSArray *accountArray = [accountStore accountsWithAccountType:accountType];
                 if (accountArray.count > 0) {
                     NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/update.json"];
                     NSDictionary *params = [NSDictionary dictionaryWithObject:@"SLRequest post test." forKey:@"status"];
                     
                     SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeTwitter
                                                             requestMethod:SLRequestMethodPOST
                                                                       URL:url
                                                                parameters:params];
                     [request setAccount:[accountArray objectAtIndex:0]];
                     [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                         NSLog(@"responseData=%@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
                     }];
                 }
             }
         }];
    }

この例では、iOS内で認証した複数アカウントが存在した場合は1件目で投稿していますが、ACAccount#identifierを保持しておいて、 ACAccountStore#accountWithIdentifier: でACAccountを取得するようにします。

また、iOS5から追加されたACAccountStore#requestAccessToAccountsWithType:withCompletionHandler:handler:はDEPRECATEDとなっているので、ACAccountStore#requestAccessToAccountsWithType:options:completion:を使うようにします。Twitterの場合はoptionsはnilで構いません。

TwitterのREST APIドキュメントはこちら ⇨ https://dev.twitter.com/docs/api/1.1/post/statuses/update


Weiboの場合

WeiboもほとんどTwitterと同様で、WeiboAPIのversion2であればappkeyは不要のようです。尚、Weiboのアカウント認証設定を出すためには中国語用のキーボードを追加すればiOSの設定画面でWeiboが出現します。

    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeSinaWeibo]) {
        ACAccountStore *accountStore = [[ACAccountStore alloc] init];
        ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierSinaWeibo];
        [accountStore
         requestAccessToAccountsWithType:accountType
         options:nil
         completion:^(BOOL granted, NSError *error) {
             if (granted) {
                 NSArray *accountArray = [accountStore accountsWithAccountType:accountType];
                 if (accountArray.count > 0) {
                     NSURL *url = [NSURL URLWithString:@"https://api.weibo.com/2/statuses/update.json"];
                     NSDictionary *params = [NSDictionary dictionaryWithObject:@"SLRequest post test." forKey:@"status"];
                     SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeSinaWeibo
                                                             requestMethod:SLRequestMethodPOST
                                                                       URL:url
                                                                parameters:params];
                     [request setAccount:[accountArray objectAtIndex:0]];
                     [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                         NSLog(@"responseData=%@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
                     }];
                 }
             }
         }];
    }

WeiboのWebAPIドキュメントはこちら ⇨ http://open.t.sina.com.cn/wiki/2/statuses/update


Facebookの場合

ACAccountStoreでアカウントにアクセスする際に、Facebookアプリ登録といくつかの設定が必要となります。

FacebookAPI (iOS SDK)を拡張するで書いたようにFacebookアプリを登録します。ソースに埋め込むApp IDは基本設定のところに表示されます。

f:id:h_mori:20121010223008p:image:w360

基本設定タブの「アプリをFacebookに結合する方法を選択」でネイティブiOSアプリを選択し、開発しているiOSアプリBundle identifierを入力します。

f:id:h_mori:20121010223009p:image:w360

詳細設定タブの認証でAppTypeをNative/Desktopに、App Secret in ClientをNoに設定します。

f:id:h_mori:20121010223010p:image:w360


    if ([SLComposeViewController isAvailableForServiceType:SLServiceTypeFacebook]) {
        ACAccountStore *accountStore = [[ACAccountStore alloc] init];
        ACAccountType *accountType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook];
        NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                                 @"2768xxxxxxxxxxx", ACFacebookAppIdKey,
                                 [NSArray arrayWithObjects:@"public_actions", @"publish_stream", @"offline_access", nil], ACFacebookPermissionsKey,
                                 ACFacebookAudienceOnlyMe, ACFacebookAudienceKey,
                                 nil];
        [accountStore
         requestAccessToAccountsWithType:accountType
         options:options
         completion:^(BOOL granted, NSError *error) {
             NSArray *accountArray = [accountStore accountsWithAccountType:accountType];
             for (ACAccount *account in accountArray) {
                 
                 NSString *urlString = [NSString stringWithFormat:@"https://graph.facebook.com/%@/feed", [[account valueForKey:@"properties"] valueForKey:@"uid"]] ;
                 NSURL *url = [NSURL URLWithString:urlString];
                 NSDictionary *params = [NSDictionary dictionaryWithObject:@"SLRequest post test." forKey:@"message"];
                 SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeFacebook
                                                         requestMethod:SLRequestMethodPOST
                                                                   URL:url
                                                            parameters:params];
                 [request setAccount:account];
                 [request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) {
                     NSLog(@"responseData=%@", [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]);
                 }];
             }
         }];
    }

optionsで、ACFacebookAppIdKey(Facebookアプリで登録した時のApp ID)、ACFacebookPermissionsKey(アプリのアクセス権限)、ACFacebookAudienceKey(投稿の公開範囲)でACFacebookAudienceEveryone/ACFacebookAudienceFriends/ACFacebookAudienceOnlyMeのいづれかを設定します。

FacebookのPermissionsの詳細についてはこちら ⇨ http://developers.facebook.com/docs/authentication/permissions/

投稿だけであれば、「public_actions」「publish_stream」「offline_access」を設定しておけば十分かと思います。


サンプルコード

サンプルをgithubにアップしました。

https://github.com/hmori/SocialPostSample

f:id:h_mori:20121011000258p:image:w360

Show accountはiOSで認証したACAccountのオブジェクトを表示します。SLComposeViewControllerの各ボタンはiOS標準の投稿画面がポップアップします。SLRequestの各ボタンは固定メッセージを直接投稿しますのでご注意ください。


また、実装してみて1つ疑問が湧いたのですが、ACAccountStoreとSLRequestを利用すればTwitter、Weiboについてはアプリ登録なし(appkey不要)で任意のメッセージを投稿できてしまいます。おそらくどのソーシャルメディアもスパム対策としてアプリから投稿する場合にユーザーが意図しない内容を勝手に投稿してはいけない規約が存在します。スパムアプリと認定された場合はそのappkeyでのAPIに制限が掛けることが可能だと思いますが、SLRequestとACAccountでの実装はappkeyなしでも投稿が可能な仕組みです。AppStoreのアプリ配布制限もあくまでApple側の管理下なのでTwitter、Weibo側からの要求がなければスパムアプリを止めることができないように思います。いづれTwitter、Weiboのカスタム実装でFacebookのようにappkeyやpermissionが必要となる日がくるかもしれません。

2012-10-04

スクロール追跡バナーを作ってみた

スマホに最適化されたWebサイトでよく見かけるスクロールについてくるあのバナーを、あえてネイティブで実装してみました。あくまで技術的なネタであってわざわざこういった実装をする目的はアレなので実際にAppStoreの審査でどうなるかは不明です。

方法としては、UIWebView(UIScrollView)とバナーのViewはそれぞれ独立させて配置し、スクロールをトラッキングするUIScrollViewDelegate#scrollViewDidScroll:(UIScrollView) で、scrollViewのoffsetを利用し、バナーのViewのoffsetを調整するようにします。


バナーを画面スクロールに追従させる

UIWebViewは全画面にADBannerViewは下部に配置します。

f:id:h_mori:20121004090548p:image:w360


UIWebViewのscrollViewのdelegateを設定しハンドリングし、scrollViewのcontentOffset.yを調整するようにします。

これでUIWebViewのトラッキングについてbannerViewが移動するようになります。

    self.webView.scrollView.delegate = self;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGRect screen = [[UIScreen mainScreen] applicationFrame];
    CGFloat newOffsetY = screen.size.height - self.bannerView.frame.size.height - scrollView.contentOffset.y;
    self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
}

この実装だけでは張り付いた状態のままなので、スクロールが止まった時点で初期位置に戻すようにします。


スクロール終了時に画面下部に移動させる

UIScrollViewDelegateのドラッグ終了、スクロール慣性の停止、タイトルバータップによるTOP移動アニメーション終了が分かれているので、それぞれで検知します。

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     //ドラッグ終了
     if(!decelerate) {
        [self resetOffsetBannerView:scrollView];
     }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
     //スクロール慣性停止
    [self resetOffsetBannerView:scrollView];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
     //TOP移動アニメーション終了
    [self resetOffsetBannerView:scrollView];
}

scrollViewDidEndDragging: willDecelerate: のdecelerateは慣性スクロールが働いているかのフラグです。ここでは慣性が働いていない時(decelerate=NO)に画面が止まったと判断して処理します。

これでWebサイトに乗っかってるかのようなあのいやらしいバナーの動きを再現できます(^^


サンプルコード

サンプルコードをgithubにアップしました。

https://github.com/hmori/AdChaseSample


f:id:h_mori:20121004091958p:image:w180

f:id:h_mori:20121004091959p:image:w180



scrollViewDidScroll: で移動するcontentOffsetの方向を判定すると、追跡するバナーの方向を上方向だけに限定することも可能ですが、ちょっとやり過ぎかもしれません。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.adjustOffset.y < scrollView.contentOffset.y) {
        CGFloat newOffsetY = [self offsetYBannerView] + self.adjustOffset.y - scrollView.contentOffset.y;
        self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
    }
}

この方法は、標準アプリの「メモ」のような画面の上部にUISearchViewを配置しスクロールに合わせて上に隠れる仕掛けと多分同じです。

スクロール追跡バナーを作ってみた

スマホに最適化されたWebサイトでよく見かけるスクロールについてくるあのバナーを、あえてネイティブで実装してみました。あくまで技術的なネタであってわざわざこういった実装をする目的はアレなので実際にAppStoreの審査でどうなるかは不明です。

方法としては、UIWebView(UIScrollView)とバナーのViewはそれぞれ独立させて配置し、スクロールをトラッキングするUIScrollViewDelegate#scrollViewDidScroll:(UIScrollView) で、scrollViewのoffsetを利用し、バナーのViewのoffsetを調整するようにします。


バナーを画面スクロールに追従させる

UIWebViewは全画面にADBannerViewは下部に配置します。

f:id:h_mori:20121004090548p:image:w360


UIWebViewのscrollViewのdelegateを設定しハンドリングし、scrollViewのcontentOffset.yを調整するようにします。

これでUIWebViewのトラッキングについてbannerViewが移動するようになります。

    self.webView.scrollView.delegate = self;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGRect screen = [[UIScreen mainScreen] applicationFrame];
    CGFloat newOffsetY = screen.size.height - self.bannerView.frame.size.height - scrollView.contentOffset.y;
    self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
}

この実装だけでは張り付いた状態のままなので、スクロールが止まった時点で初期位置に戻すようにします。


スクロール終了時に画面下部に移動させる

UIScrollViewDelegateのドラッグ終了、スクロール慣性の停止、タイトルバータップによるTOP移動アニメーション終了が分かれているので、それぞれで検知します。

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     //ドラッグ終了
     if(!decelerate) {
        [self resetOffsetBannerView:scrollView];
     }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
     //スクロール慣性停止
    [self resetOffsetBannerView:scrollView];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
     //TOP移動アニメーション終了
    [self resetOffsetBannerView:scrollView];
}

scrollViewDidEndDragging: willDecelerate: のdecelerateは慣性スクロールが働いているかのフラグです。ここでは慣性が働いていない時(decelerate=NO)に画面が止まったと判断して処理します。

これでWebサイトに乗っかってるかのようなあのいやらしいバナーの動きを再現できます(^^


サンプルコード

サンプルコードをgithubにアップしました。

https://github.com/hmori/AdChaseSample


f:id:h_mori:20121004091958p:image:w360:left

f:id:h_mori:20121004091959p:image:w360:left



scrollViewDidScroll: で移動するcontentOffsetの方向を判定すると、追跡するバナーの方向を上方向だけに限定することも可能ですが、ちょっとやり過ぎかもしれません。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.adjustOffset.y < scrollView.contentOffset.y) {
        CGFloat newOffsetY = [self offsetYBannerView] + self.adjustOffset.y - scrollView.contentOffset.y;
        self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
    }
}

この方法は、標準アプリの「メモ」のような画面の上部にUISearchViewを配置しスクロールに合わせて上に隠れる仕掛けと多分同じです。

スクロール追跡バナーを作ってみた

スマホに最適化されたWebサイトでよく見かけるスクロールについてくるあのバナーを、あえてネイティブで実装してみました。あくまで技術的なネタであってわざわざこういった実装をする目的はアレなので実際にAppStoreの審査でどうなるかは不明です。

方法としては、UIWebView(UIScrollView)とバナーのViewはそれぞれ独立させて配置し、スクロールをトラッキングするUIScrollViewDelegate#scrollViewDidScroll:(UIScrollView) で、scrollViewのoffsetを利用し、バナーのViewのoffsetを調整するようにします。


バナーを画面スクロールに追従させる

UIWebViewは全画面にADBannerViewは下部に配置します。

f:id:h_mori:20121004090548p:image:w360


UIWebViewのscrollViewのdelegateを設定しハンドリングし、scrollViewのcontentOffset.yを調整するようにします。

これでUIWebViewのトラッキングについてbannerViewが移動するようになります。

    self.webView.scrollView.delegate = self;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGRect screen = [[UIScreen mainScreen] applicationFrame];
    CGFloat newOffsetY = screen.size.height - self.bannerView.frame.size.height - scrollView.contentOffset.y;
    self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
}

この実装だけでは張り付いた状態のままなので、スクロールが止まった時点で初期位置に戻すようにします。


スクロール終了時に画面下部に移動させる

UIScrollViewDelegateのドラッグ終了、スクロール慣性の停止、タイトルバータップによるTOP移動アニメーション終了が分かれているので、それぞれで検知します。

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     //ドラッグ終了
     if(!decelerate) {
        [self resetOffsetBannerView:scrollView];
     }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
     //スクロール慣性停止
    [self resetOffsetBannerView:scrollView];
}

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
     //TOP移動アニメーション終了
    [self resetOffsetBannerView:scrollView];
}

scrollViewDidEndDragging: willDecelerate: のdecelerateは慣性スクロールが働いているかのフラグです。ここでは慣性が働いていない時(decelerate=NO)に画面が止まったと判断して処理します。

これでWebサイトに乗っかってるかのようなあのいやらしいバナーの動きを再現できます(^^


サンプルコード

サンプルコードをgithubにアップしました。

https://github.com/hmori/AdChaseSample

scrollViewDidScroll: で移動するcontentOffsetの方向を判定すると、追跡するバナーの方向を上方向だけに限定することも可能ですが、ちょっとやり過ぎかもしれません。

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (self.adjustOffset.y < scrollView.contentOffset.y) {
        CGFloat newOffsetY = [self offsetYBannerView] + self.adjustOffset.y - scrollView.contentOffset.y;
        self.bannerView.frame = CGRectMake(self.bannerView.frame.origin.x,
                                           newOffsetY,
                                           self.bannerView.frame.size.width,
                                           self.bannerView.frame.size.height);
    }
}

この方法は、標準アプリの「メモ」のような画面の上部にUISearchViewを配置しスクロールに合わせて上に隠れる仕掛けと多分同じです。

2012-03-16

緊急時に10秒でマルチ送信できる無料アプリ「生存連絡」をリリースしました

東日本大震災から1年が経ちました。あらためてお亡くなりになられた方々のご冥福をお祈り申し上げます。

当日、私は仙台市で務めていた会社のビル8階で地震に見舞われたのですが、今までに体験したことのないような状況でおそらく一生忘れることはないと思います。2分近い揺れの中で机の下に潜り死を意識した瞬間もあるくらいでしたので、1階に避難する際にもかなり動揺していたことを覚えています。自分の安全を確保してまず行うことは家族の安否連絡なのですが当然のように電話は通じませんでした。ネット回線はまだ可能な状態でしたのでとりあえずはTwitterとFacebookに自分が無事であるメッセージを残したのですが、動揺と地面の揺れで上手く操作が行えなかったことを記憶しています。

最近また地震がちょくちょくあり全体的に防災意識が高まっていると思いますので、今回「緊急異常事態時にすぐさまにメッセージを送信できるように」というiPhoneアプリを無料でリリースしました。事前に送信先・連絡先を設定しておき、緊急時にメッセージを送信するというアプリなのですが、慌てていても10秒以内にメール、SMSメッセージ、Twitter、Facebookに同時送信することができます。



生存連絡

f:id:h_mori:20120316100953p:image:medium:left


インストール (無料)





大災害の時の安否連絡は電話よりもインターネットを利用した伝達手段が確実ですが、落ち着いてメッセージを入力したりするのは困難です。東日本大震災の発生直後では「連絡先が直ぐに出てこない」「気が動転して上手く操作できない」などの声が聞かれました。このアプリでは送信先を事前に登録してメッセージを選択するだけの簡単操作でわずか10秒で生存連絡などが送信できます。

災害を事前に備えましょう。

=主な機能=

・文章選択によるメッセージ作成機能

・メッセージのマルチポスト送信機能

・現在地発信機能

=送信先媒体=

・Email

・ショートメール

・Twitter

・Facebook

インストール後は最初に送信先の設定を行なってください。


スクリーンショット

f:id:h_mori:20120316100954p:image:w240:left

f:id:h_mori:20120316100955p:image:w240:left

f:id:h_mori:20120316100956p:image:w240



実際に使われるような状況は起きてほしくないと心から願ってるのですが、このアプリをインストールして設定することで安心が得られるのなら幸いです。