Background Fetch

New Multitasking APIs

iOS 7から追加されたマルチタスクAPIの「Background Fetch」について書きます。


概要

「Background Fetch」は、ユーザーがアプリを開く前に、コンテンツをアップデートできるようにします。そうすることで、新しいコンテンツが、ユーザーに見てもらうのを待つ状態をつくります。

実装方法

  1. Xcode > Target > Capabilities > Background ModesをONにして、Background fetchをチェックする
  1. Background fetchを有効にする(インターバルを設定)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Set background fetch interval
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
}

Background Fetch interval minimum

  1. アプリケーションから呼ばれるデリゲートメソッドを実装する
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    // Retrieve content
    …

    // Must call completion handler
    completionHandler(UIBackgroundFetchResultNewData);
}

※上記のデリゲートメソッドが呼ばれるタイミングは、システムがユーザーの行動パターンを分析し、その結果を元に、アプリが起動される前のタイミングに呼ばれるようになっているようです。 Fetch timing

Background fetchの使いどころ

Appleのドキュメントでは以下のケースで、Background fetchを利用することを推奨していました。 - Social network feeds - News and entertainment - Blog aggregators - Weather - Finance

デバッグ方法

Xcode > Debug > Simulate Background Fetchを選択するとデリゲートメソッドが呼ばれます。

所感

こちらの技術ブログで現在の歩数をバックグラウンドでツイートするといったアイディアが書かれていました。他にも有用な利用方法があるかどうか、考えてみたいと思います。

参考情報

http://qiita.com/griffin_stewie/items/8371c09059b3ba7bb202
WWDC 2013 What’s New with Multitasking Session 204

Xcode5 Debugger tips

Xcode5 LLDBデバッガでは、デバッグ中に画像、パスのプレビューが出来るようになっています。

  • 画像のプレビュー
  • PDFのプレビュー (不可)
  • パスのプレビュー

※UIBezierPathのプレビューは出来ないようです。Xcodeがクラッシュします。


参考情報

WWDC 2013 What’s New in Xcode 5 session 400

Apple Game Centerサービスのリーダーボードを使ってみた

Game Centerのリーダーボードについて説明します。


Game Center

Game CenterはiOS4.x以降から利用できる、Apple社が提供するゲームSNSサービスです。Game CenterにApple IDでサインインすることでiOSのGame Center対応アプリと連携し、アプリ側からスコアを保存することができ、ランキング表示、オンライン対戦等の実装が可能になります。


Game Centerの主な機能

  • リーダーボード
  • アチーブメント
  • チャレンジ
  • マッチメーク
  • リアルタイム対戦
  • ホスト型の対戦
  • ターン制の対戦

リーダーボードについて

ゲームスコアのランキング用レコードで、1アプリにつき、最大500個作成できる。レコードを複数のアプリで共有することもできる。


保存できるデータ

スコアデータとして64bit整数のみ保存可能、また、スコアデータの他にスコアコンテキストというものがあり、同じく64bit整数が保存できる。


ログイン認証

Sample code


// Authenticate
- (void)authenticateLocalPlayer
{
    GKLocalPlayer  __weak *localPlayer;
    localPlayer = [GKLocalPlayer localPlayer];
    localPlayer.authenticateHandler = ^(UIViewController* viewController, NSError* error){
        if (nil != viewController) {
            // Show authentication dialog controller
            if ([self.delegate respondsToSelector:@selector(gameCenterManagerRequestedShowAuthenticationDialog:)]) {
                // Notify it
                [self.delegate gameCenterManagerRequestedShowAuthenticationDialog:viewController];
            }
        }
        else if (localPlayer.authenticated) {
            // Load friends
            [localPlayer loadFriendsWithCompletionHandler:^(NSArray* friendIDs, NSError* error) {
                // Notify
                if ([self.delegate respondsToSelector:@selector(gameCenterManagerAuthenticateSuccess:)]) {
                    // Notify it
                    [self.delegate gameCenterManagerAuthenticateSuccess:localPlayer];
                }
            }];
        }
        else {
            if ([self.delegate respondsToSelector:@selector(gameCenterManagerAuthenticateFaild)]) {
                // Notify it
                [self.delegate gameCenterManagerAuthenticateFaild];
            }
        }
    };
}

CKLocalPlayerを作成し、authenticateHandlerプロパティにBlockを設定する。Block引数の「viewController」が認証用コントローラーとなっている。
↓認証用コントローラーのキャプチャ


ランキングの表示方法

・GKGameCenterViewControllerを使うのが簡単。GameCenterを利用するための一通りのUIを提供してくれる。

Sample code


- (void)_showgameCenterController
{
    GKGameCenterViewController *gameCenterController = [[GKGameCenterViewController alloc] init];
    if (nil != gameCenterController)
    {
        gameCenterController.gameCenterDelegate = self;
        gameCenterController.viewState = GKGameCenterViewControllerStateLeaderboards;
        
        [self presentViewController: gameCenterController animated: YES completion:nil];
    }
}

↓GKGameCenterViewControllerのキャプチャ


ランキング表示を独自実装するために用いるGame Kitのクラス

ランキング表示を独自で実装する場合は、下記のクラスを使います。

クラス名 クラスの機能
GKScore ゲーム本体の中でGKScoreオブジェクトを生成し、こ れを使ってGame Center上のLeaderboardにスコアを送信します。逆に Leaderboardからスコア情報を取得する場合も、スコアはGKScoreオブ ジェクトの形で得られます。
GKLeaderboard Leaderboardからスコア データを検索する際にGKLeaderboardオブジェクトを生成しま す。
GKLocalPlayer GameCenterにサインインしているユーザーの詳細情報を管理します

Sample Code

スコアの送信


// Create score
GKScore *scoreReporter = [[GKScore alloc] initWithLeaderboardIdentifier:identifier];

// Set score value
scoreReporter.value = score;

// Set context value
scoreReporter.context = 0;

// Report score
[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {}];

ランキングの取得


// Create leaderboard
GKLeaderboard *leaderboardRequest = [[GKLeaderboard alloc] init];

// Set player scope (GKLeaderboardPlayerScopeGlobal or GKLeaderboardPlayerScopeFriendsOnly)
leaderboardRequest.playerScope = GKLeaderboardPlayerScopeGlobal;

// Specifying the duration
leaderboardRequest.timeScope = GKLeaderboardTimeScopeToday;

// Decide record from which to get
leaderboardRequest.identifier = @"Combined.LandMaps"

// Specifies the top 10
leaderboardRequest.range = NSMakeRange(1,10);

// Request
[leaderboardRequest loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error) {
// スコア情報を処理する。
}];


所感

サーバを用意しなくても、アプリにランキング機能をつけられるので便利です。欲を言うと、単純な整数だけでなく、文字列も送れるようになってほしい。


参考情報

https://developer.apple.com/jp/devcenter/ios/library/documentation/GameKit_Guide.pdf

https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnectGameCenter_Guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40013726

https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/GameKit_Guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008304

Strech Button

UIDynamicsフレームワークのUISnapBehaviorを使って伸び縮みするボタンのようなものを作りました。

(↓ぐにょ〜っとした物体)


UISnapBehaviorとは

UISnapBehaviorは、Viewに設定するだけで、バネのような動きをリアルに再現してくれます

(↓1つのViewにUISnapBehaviorを適用した例)


作り方(概要)

UISnapBehaviorを適用したViewを円状に配置します。

Viewをドラッグさせる必要があるため、それぞれのViewにUIPanGestureRecognizerを追加します。

ドラッグ中、UIPanGestureRecognizerのセレクター内で、それぞれのViewの座標を設定します。座標の設定には加速度を利用します、加速度はドラッグされているViewに近いものほど大きく、遠いものほど小さくなるような計算式を使います。

(↓こんな感じに)

ドラッグ中のViewの座標の計算は必要ですが、UISnapBehaviorを利用することにより、ドラッグが終わった後、Viewがバネのような動きを伴って元の位置に戻るアニメーションを実装をしなくていいという点が、一番のメリットです。
このバネのような動きがリアルにできていてすごいです!


Viewの座標をもとにパスを描画します

(↓こんな感じに)


最後に、Viewを消して、パスを塗りつぶせば完成です!


所感

UIDynamicsを利用すれば色々と面白い動きをしたものが手軽に出来そうだと、あらためて思いました。また何か思いついたら作ってみます。


UICollectionViewのレイアウト変更について

iOS 7から新しく追加されたAPIを使い、UICollectionViewのレイアウト変更をUINavigationViewControllerと連携させる方法について説明します。


UICollectionViewのレイアウトを変更する際、UICollectionViewのuseLayoutToLayoutNavigationTransitionsプロパティを使うと、UINavigationControllerと連携することができます。 UINavigationControllerと連携することで、実装が安易になる、UIが分かりやすい、といったメリットがあります。

以下、useLayoutToLayoutNavigationTransitionsプロパティについて説明している、ドキュメントの冒頭です。


useLayoutToLayoutNavigationTransitions

This property helps facilitate transitions between two or more collection view controllers using a navigation controller. 


useLayoutToLayoutNavigationTransitionsプロパティを使った場合

useLayoutToLayoutNavigationTransitionsプロパティを使わない場合


Code Example


// Cooperate with navigation controller to switch collection view layout
/////////////////////////////////////////////////////////////////////////////////
newCollectionViewcontroller.useLayoutToLayoutNavigationTransitions = YES;
/////////////////////////////////////////////////////////////////////////////////

// Push collection view controller
[self.navigationController pushViewController:newCollectionViewcontroller animated:YES];

useLayoutToLayoutNavigationTransitionsプロパティをYESに設定し、NavigationControllerにCollectionViewControllerをプッシュします。
プッシュするとレイアウトがアニメーションを伴って変更されます。
このとき、ナビゲーションバーに戻るボタンが表示され、表示された戻るボタンをタップすることで元のレイアウトに戻すことが出来ます。

注意点

※コントローラをプッシュしていますが、最初のCollectionViewが使い回されているようです。

検証ログ

-[CollectionViewController viewWillAppear:] : 47
------------------------------------------
self.navigationController.viewControllers.count : 1
self : <CollectionViewController: 0x10e81c1c0>
self.collectionView : <UICollectionView: 0x10c04a800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x10ae79ec0>; layer = <CALayer: 0x10ae77f90>; contentOffset: {0, -64}> collection view layout: <UICollectionViewFlowLayout: 0x10e81b830>
self.collectionView.dataSource : <CollectionViewController: 0x10e81c1c0>
self.collectionView.delegate : <CollectionViewController: 0x10e81c1c0>
-[CollectionViewController viewWillAppear:] : 47
------------------------------------------
self.navigationController.viewControllers.count : 2
self : <CollectionViewController: 0x108fd3e70>
self.collectionView : <UICollectionView: 0x10c04a800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x10ae79ec0>; layer = <CALayer: 0x10ae77f90>; contentOffset: {0, -64}> collection view layout: <UICollectionViewFlowLayout: 0x10e81b830>
self.collectionView.dataSource : <CollectionViewController: 0x10e81c1c0>
self.collectionView.delegate : <CollectionViewController: 0x10e81c1c0>
-[CollectionViewController viewWillAppear:] : 47
------------------------------------------
self.navigationController.viewControllers.count : 3
self : <CollectionViewController: 0x10ad18f00>
self.collectionView : <UICollectionView: 0x10c04a800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x10ae79ec0>; layer = <CALayer: 0x10ae77f90>; contentOffset: {0, -38}> collection view layout: <UICollectionViewFlowLayout: 0x108fd3ad0>
self.collectionView.dataSource : <CollectionViewController: 0x10e81c1c0>
self.collectionView.delegate : <CollectionViewController: 0x108fd3e70>
-[CollectionViewController viewWillAppear:] : 47
------------------------------------------
self.navigationController.viewControllers.count : 4
self : <CollectionViewController: 0x108fdc660>
self.collectionView : <UICollectionView: 0x10c04a800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x10ae79ec0>; layer = <CALayer: 0x10ae77f90>; contentOffset: {0, -3}> collection view layout: <UICollectionViewFlowLayout: 0x10ad18b60>
self.collectionView.dataSource : <CollectionViewController: 0x10e81c1c0>
self.collectionView.delegate : <CollectionViewController: 0x10ad18f00>
-[CollectionViewController viewWillAppear:] : 47
------------------------------------------
self.navigationController.viewControllers.count : 5
self : <CollectionViewController: 0x10ae06110>
self.collectionView : <UICollectionView: 0x10c04a800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x10ae79ec0>; layer = <CALayer: 0x10ae77f90>; contentOffset: {0, 102}> collection view layout: <UICollectionViewFlowLayout: 0x108f9db00>
self.collectionView.dataSource : <CollectionViewController: 0x10e81c1c0>
self.collectionView.delegate : <CollectionViewController: 0x108fdc660>

Not cooperating UINavigationController

ナビゲーションコントローラーと連動させずにレイアウトを変更するには、以下のメソッドを使用します。

  • (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated; (Available IOS 6)
  • (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated completion:(void (^)(BOOL finished))completion; (Available iOS 7)

Multipeer Connectivityを試してみた

Multipeer Connectivity Framework

Multipeer Connectivityを試してみた


iOS 7に追加されたフレームーワークでWifiBluetoothを利用した近距離にあるiOS端末間のPeer to Peer 通信を手軽に実現するためのフレームーワークです。


ユースケース

  • 文書、写真の共同編集作業
  • ファイル共有
  • センサーデータの集積

DEMO

Multipeer Connectivity Framework API クラス群

MCPeerID

MCSession

  • 複数のピア間の通信を管理

MCAdvertiserAssistant

  • ピアを他のピアから存在を発見できるようにする(アドバタイズ)
  • 接続要求に対する処理

MCBrowserViewController

  • ピアの検索
  • ピアのリスト表示
  • 接続要求の送信
  • 接続の確立

接続方法

接続要求を受け取る準備

  1. peerIDを作成します

_peerID = [[MCPeerID alloc] initWithDisplayName:@"displayName"];

  1. セッションを作成します

_session = [[MCSession alloc] initWithPeer:_peerID securityIdentity:nil encryptionPreference:MCEncryptionRequired];
_session.delegate = self;

  1. ピアを他のピアから存在を発見できるようにします

_advertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:@"serviceType" discoveryInfo:nil session:_session];
[_advertiserAssistant start];

接続要求を送信する

  1. MCBrowserViewControllerを作成/表示する

    MCBrowserViewController *browserViewController;
    browserViewController = [[MCBrowserViewController alloc] initWithServiceType:_serviceType session:_session];
    browserViewController.delegate = self;
    browserViewController.minimumNumberOfPeers = kMCSessionMinimumNumberOfPeers;
    browserViewController.maximumNumberOfPeers = kMCSessionMaximumNumberOfPeers;      
[self presentViewController:browserViewController animated:YES completion:nil];

- MCBrowserViewControllerのUI
  1. MCBrowserViewControllerが表示されると自動的に近くの端末が検出されます、検出された端末をタップすると、接続要求が送信されます。
    • 接続要求受信時のキャプチャ

接続状態

ピアの接続状態が変化するとMCSessionのデリゲートメッソドが呼ばれる


-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state

データの送受信

テキストの送信


// Send a data message to a list of destination peers
- (BOOL)sendData:(NSData *)data toPeers:(NSArray *)peerIDs withMode:(MCSessionSendDataMode)mode error:(NSError **)error;

テキストの受信


// テキストを受け取ったとき呼ばれる
- (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID

リソースの送信


Send a resource (a file or an HTTP document) referenced by an NSURL; 
 completionHandler is called when the resource is received by the remote
 peer or if an error occurred.
- (NSProgress *)sendResourceAtURL:(NSURL *)resourceURL withName:(NSString *)resourceName toPeer:(MCPeerID *)peerID withCompletionHandler:(void(^)(NSError *error))completionHandler;

リソースの受信


- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress

-(void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error

バイトストリームの送信


Opens a byte stream to a nearby peer.
- (NSOutputStream *)startStreamWithName:(NSString *)streamName toPeer:(MCPeerID *)peerID error:(NSError **)error

バイトストリームの受信


Called when a nearby peer opens a byte stream connection to the local peer.
-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID


サンプル

More Information

https://developer.apple.com/library/prerelease/ios https://developer.apple.com/library/prerelease/ios/samplecode/MultipeerGroupChat/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013691

参考情報

http://tech.voyagegroup.com/archives/7333988.html http://dev.classmethod.jp/references/ios-multipeer-apiusage/

iOS 7のMessage appのようなUIを作ってみた

UICollectionView + UIKitDynamics


実装方法

UICollectionViewFlowLayoutとUIKitDynamicsを連携させます。

  1. UICollectionviewFlowLayoutのサブクラスを作成し、CollectionViewに設定する。
  2. 作成したサブクラスは、UIDyanicsAnimatorのインスタンスを持ち、LayoutAttributesに対して、DynamicBehaviorを作成し、UIDynamicsAnimatorが管理する。
  3. CollectionViewには、UIDynamicAnimatorの管理するDynamicBehaviorのLayoutAttributesオブジェクトを返すようにする。

手順

UICollectionviewFlowLayoutのサブクラスを作成する


@interface DynamicsFlowLayout : UICollectionViewFlowLayout
    @property (nonatomic) UIDynamicAnimator* animator;
    @property (nonatomic) NSMutableSet* visibleIndexPathsSet;
    @property (nonatomic) CGFloat latestDelta;
@end

  • 作成したLayoutクラスにダイナミックアニメーターのインスタンスを保持する

- (id)init
{
    self = [super init];
    if (nil == self) {
        return nil;
    }
    
    // Create dynamic animator
    self.animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
    self.visibleIndexPathsSet = [NSMutableSet set];
    
    // Configure self
    self.itemSize = CGSizeMake(310, 60);
    self.minimumInteritemSpacing = 1;
    self.minimumLineSpacing = 5;
    
    return self;
}


prepareLayoutメソッドをオーバーライドしUIAttachmentBehaviorを作成する

  • 画面に表示されなくなったbehaiviorを削除する

- (void)prepareLayout
{
    // Invoke super
    [super prepareLayout];
    
    // Need to overflow our actual visible rect slightly to avoid flickering.
    // Get visible rect
    CGRect  originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
    CGRect  visibleRect = CGRectInset(originalRect, -100, -100);
    
    // Get attributes in visible rect
    NSArray*    itemsInVisibleRectArray;
    itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];
    
    // Get their index paths
    NSSet   *itemsIndexPathsInVisibleRectSet;
    itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];
    
    
    //
    // Step 1: Remove any behaviours that are no longer visible.
    //
    NSPredicate*    predicate;
    predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior* behaviour, NSDictionary* bindings){
        BOOL    currentlyVisible = ([itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil);
        return !currentlyVisible;
    }];
    NSArray*    noLongerVisibleBehaviours;
    noLongerVisibleBehaviours = [self.animator.behaviors filteredArrayUsingPredicate:predicate];
    [noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop){
        [self.animator removeBehavior:obj];
        [self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
    }];
    
                                …中略…
} 

  • 新しく画面に表示されるattributeから、behaviourを作成する

//
    // Step 2: Add any newly visible behaviours.
    // A "newly visible" item is one that is in the itemsInVisibleRect(Set|Array) but not in the visibleIndexPathsSet
    predicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes* item, NSDictionary* bindings){
        BOOL    currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
        return !currentlyVisible;
    }];
    NSArray*    newlyVisibleItems;
    newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:predicate];
    
    
    // Get touch location
    CGPoint touchLocation;
    touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
    
    
    [newlyVisibleItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes* item, NSUInteger idx, BOOL *stop){
        
        // Get center
        CGPoint center = item.center;
        
        // Create attachment behavior
        UIAttachmentBehavior *springBehaviour = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
        
        // Configure behaviour
        springBehaviour.length = 0.0f;
        springBehaviour.damping = 0.8f;
        springBehaviour.frequency = 1.0f;
        
        if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
            CGFloat yDistanceFromTouch;
            CGFloat xDistanceFromTouch;
            CGFloat scrollResistance;
            yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
            xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
            scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
            
            if (self.latestDelta < 0) {
                center.y += MAX(self.latestDelta, self.latestDelta * scrollResistance);
            }
            else {
                center.y += MIN(self.latestDelta, self.latestDelta * scrollResistance);
            }
            item.center = center;
        }
        [self.animator addBehavior:springBehaviour];
        [self.visibleIndexPathsSet addObject:item.indexPath];
    }];

  • layoutAttributesForElementsInRect:メソッドをオーバーライドし、dynamic animatorのitemsInRect:メソッドを呼び、dynamic animatorに追加されているattachment behaviorのlayout attributesを返す

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
{
    return [self.animator itemsInRect:rect];
}

  • layoutAttributesForItemAtIndexPath:メソッドをオーバーライドし、dynamic animatorのlayoutAttributesForCellAtIndexPath:メソッドを呼び、indexPathで指定されたlayout attributesを返す

- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [self.animator layoutAttributesForCellAtIndexPath:indexPath];
}

  • shouldInvalidateLayoutForBoundsChange:メソッドをオーバーライドし、layout attributeの位置を更新する

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
    // Calc content offset's y
    UIScrollView*   scrollView;
    CGFloat         delta;
    scrollView = self.collectionView;
    delta = newBounds.origin.y - scrollView.bounds.origin.y;
    
    self.latestDelta = delta;
    
    // Get touch location
    CGPoint touchLocation;
    touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
    
    [self.animator.behaviors enumerateObjectsUsingBlock:^(UIAttachmentBehavior *springBehaviour, NSUInteger idx, BOOL * stop){
        // Calc distancez
        CGFloat yDistanceFromTouch;
        CGFloat xDistanceFromTouch;
        CGFloat scrollResistance;
        yDistanceFromTouch = fabsf(touchLocation.y - springBehaviour.anchorPoint.y);
        xDistanceFromTouch = fabsf(touchLocation.x - springBehaviour.anchorPoint.x);
        scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
        
        // Get layout attribute
        UICollectionViewLayoutAttributes*   item;
        item = springBehaviour.items.firstObject;
        
        // Decide layout attribute center
        CGPoint center = item.center;
        if (delta < 0) {
            center.y += MAX(delta, delta * scrollResistance);
        }
        else {
            center.y += MIN(delta, delta * scrollResistance);
        }
        item.center = center;
        
        // Update dynamic item
        [self.animator updateItemUsingCurrentState:item];
    }];
    
    return NO;
}

以上でiOS 7のMessage appのようなUIが実現できます。



パフォーマンスについて

prepareLayout:で画面の外に消えたattributesのdynamic behaviorをanimatorからremoveし、 dynamic behaviorを追加する際は、新しく画面に表示される分だけ追加するようにすることで パフォーマンスを考慮した実装となっている。参考情報がわかりやすいのです。


所感

UIDynamicsAnimatorがUICollectionViewLayoutとシームレスに連携できるようになっていてすごい。


参考情報

obj io
UICollectionView-Spring-Demo