Skip to content
This repository has been archived by the owner on Sep 18, 2018. It is now read-only.

Commit

Permalink
Animate layout during collection/table view cell updates
Browse files Browse the repository at this point in the history
Using method swizzling to hook into animated cell updates of UICollectionView and UITableView.

- The implementation for UICollectionView is based on public API ([UICollectionViewLayout finalizeCollectionViewUpdates] and should not cause any problems.

- The implementation for UITableView swizzles a private method ([UITableView _endCellAnimationsWithContext:]. The swizzled method also has to create its own animation block to update the container view's contentSize because there is no delegate method to run an animation inside the table view's animation block.
  • Loading branch information
ole committed Jul 25, 2014
1 parent 33fb96b commit 2db7ddb
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 1 deletion.
9 changes: 9 additions & 0 deletions OLEContainerScrollView/OLEContainerScrollView+Swizzling.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
OLEContainerScrollView
Copyright (c) 2014 Ole Begemann.
https://github.com/ole/OLEContainerScrollView
*/

void swizzleUICollectionViewLayoutFinalizeCollectionViewUpdates();
void swizzleUITableView();
69 changes: 69 additions & 0 deletions OLEContainerScrollView/OLEContainerScrollView+Swizzling.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
OLEContainerScrollView
Copyright (c) 2014 Ole Begemann.
https://github.com/ole/OLEContainerScrollView
*/

@import UIKit;

#import "OLEContainerScrollView+Swizzling.h"
#import "OLESwizzling.h"
#import "OLEContainerScrollView.h"
#import "OLEContainerScrollViewContentView.h"

void swizzleUICollectionViewLayoutFinalizeCollectionViewUpdates()
{
Class classToSwizzle = [UICollectionViewLayout class];
SEL selectorToSwizzle = @selector(finalizeCollectionViewUpdates);

__block IMP originalIMP = NULL;
originalIMP = OLEReplaceMethodWithBlock(classToSwizzle, selectorToSwizzle, ^(UICollectionViewLayout *_self) {
// Call original implementation
((void ( *)(id, SEL))originalIMP)(_self, selectorToSwizzle);

// Manually set the collection view's contentSize to its new size (after the updates have been performed) to cause
// a relayout of all views in the container scroll view.
// We do this to animate the resizing of the collection view and its adjacent views in the container scroll view
// in sync with the cell update animations (finalizeCollectionViewUpdates is called inside the animation block).
// If we don't do this, the collection view will set its new content size only after the cell update animations
// have finished, which is too late for us.
UICollectionView *collectionView = _self.collectionView;
BOOL collectionViewIsInsideOLEContainerScrollView = [collectionView.superview isKindOfClass:[OLEContainerScrollViewContentView class]];
if (collectionViewIsInsideOLEContainerScrollView) {
collectionView.contentSize = _self.collectionViewContentSize;
}
});
}

@interface UITableView (Swizzling)
- (void)_endCellAnimationsWithContext:(id)context;
@end

void swizzleUITableView()
{
Class classToSwizzle = [UITableView class];
NSString *obfuscatedSelector = [NSString stringWithFormat:@"_%@llAnimat%@hContext:", @"endCe", @"ionsWit"];
SEL selectorToSwizzle = NSSelectorFromString(obfuscatedSelector);

__block IMP originalIMP = NULL;
originalIMP = OLEReplaceMethodWithBlock(classToSwizzle, selectorToSwizzle, ^(UITableView *_self, id context) {
// Call original implementation
((void ( *)(id, SEL, id))originalIMP)(_self, selectorToSwizzle, context);

[UIView animateWithDuration:0.25 animations:^{

// Manually set the collection view's contentSize to its new size (after the updates have been performed) to cause
// a relayout of all views in the container scroll view.
// We do this to animate the resizing of the collection view and its adjacent views in the container scroll view
// in sync with the cell update animations (finalizeCollectionViewUpdates is called inside the animation block).
// If we don't do this, the collection view will set its new content size only after the cell update animations
// have finished, which is too late for us.
BOOL tableViewIsInsideOLEContainerScrollView = [_self.superview isKindOfClass:[OLEContainerScrollViewContentView class]];
if (tableViewIsInsideOLEContainerScrollView) {
NSString *obfuscatedPropertyKey = [NSString stringWithFormat:@"_%@entSize", @"cont"];
_self.contentSize = [[_self valueForKey:obfuscatedPropertyKey] CGSizeValue];
}
}];
});
}
15 changes: 14 additions & 1 deletion OLEContainerScrollView/OLEContainerScrollView.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "OLEContainerScrollView.h"
#import "OLEContainerScrollView_Private.h"
#import "OLEContainerScrollViewContentView.h"
#import "OLEContainerScrollView+Swizzling.h"

@interface OLEContainerScrollView ()

Expand All @@ -20,7 +21,15 @@ @interface OLEContainerScrollView ()

@implementation OLEContainerScrollView

static void *KVOContext = &KVOContext;
+ (void)initialize
{
// +initialize can be called multiple times if subclasses don't implement it.
// Protect against multiple calls
if (self == [OLEContainerScrollView self]) {
swizzleUICollectionViewLayoutFinalizeCollectionViewUpdates();
swizzleUITableView();
}
}

- (void)dealloc
{
Expand Down Expand Up @@ -87,6 +96,8 @@ - (void)willRemoveSubviewFromContainer:(UIView *)subview

#pragma mark - KVO

static void *KVOContext = &KVOContext;

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == KVOContext) {
Expand All @@ -97,6 +108,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
CGSize newContentSize = scrollView.contentSize;
if (!CGSizeEqualToSize(newContentSize, oldContentSize)) {
[self setNeedsLayout];
[self layoutIfNeeded];
}
} else if ([keyPath isEqualToString:NSStringFromSelector(@selector(frame))] ||
[keyPath isEqualToString:NSStringFromSelector(@selector(bounds))]) {
Expand All @@ -105,6 +117,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
CGRect newFrame = subview.frame;
if (!CGRectEqualToRect(newFrame, oldFrame)) {
[self setNeedsLayout];
[self layoutIfNeeded];
}
}
} else {
Expand Down
10 changes: 10 additions & 0 deletions OLEContainerScrollView/OLESwizzling.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
OLEContainerScrollView
Copyright (c) 2014 Ole Begemann.
https://github.com/ole/OLEContainerScrollView
*/

#import <objc/runtime.h>

IMP OLEReplaceMethodWithBlock(Class c, SEL origSEL, id block);
29 changes: 29 additions & 0 deletions OLEContainerScrollView/OLESwizzling.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
OLEContainerScrollView
Copyright (c) 2014 Ole Begemann.
https://github.com/ole/OLEContainerScrollView
*/

#import "OLESwizzling.h"

// Adapted from Peter Steinberger's PSPDFReplaceMethodWithBlock
// See http://petersteinberger.com/blog/2014/a-story-about-swizzling-the-right-way-and-touch-forwarding/
IMP OLEReplaceMethodWithBlock(Class c, SEL origSEL, id block)
{
NSCParameterAssert(block);

// get original method
Method origMethod = class_getInstanceMethod(c, origSEL);
NSCParameterAssert(origMethod);

// convert block to IMP trampoline and replace method implementation
IMP newIMP = imp_implementationWithBlock(block);

// Try adding the method if not yet in the current class
if (!class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) {
return method_setImplementation(origMethod, newIMP);
}else {
return method_getImplementation(origMethod);
}
}
12 changes: 12 additions & 0 deletions OLEContainerScrollViewDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
5D2A88E8191D1DC300CEE791 /* OLEBorderedView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D2A88E7191D1DC300CEE791 /* OLEBorderedView.m */; };
5D2A88EB192246D700CEE791 /* SimpleTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D2A88EA192246D700CEE791 /* SimpleTableViewController.m */; };
5D63B223192250990064DB38 /* NaiveContainerScrollViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D63B222192250990064DB38 /* NaiveContainerScrollViewController.m */; };
5D8CFD6E19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8CFD6D19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.m */; };
5D8CFD7419815DA800B3B7F7 /* OLESwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8CFD7319815DA800B3B7F7 /* OLESwizzling.m */; };
5D8CFD7719815E5300B3B7F7 /* OLEContainerScrollViewContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8CFD7619815E5300B3B7F7 /* OLEContainerScrollViewContentView.m */; };
5D8CFD7B1981729D00B3B7F7 /* MultipleTableViewsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D8CFD7A1981729D00B3B7F7 /* MultipleTableViewsViewController.m */; };
5DBDF1E518D5F26B005B1E18 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DBDF1E418D5F26B005B1E18 /* Foundation.framework */; };
Expand Down Expand Up @@ -53,6 +55,10 @@
5D2A88EA192246D700CEE791 /* SimpleTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SimpleTableViewController.m; sourceTree = "<group>"; };
5D63B221192250990064DB38 /* NaiveContainerScrollViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NaiveContainerScrollViewController.h; sourceTree = "<group>"; };
5D63B222192250990064DB38 /* NaiveContainerScrollViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NaiveContainerScrollViewController.m; sourceTree = "<group>"; };
5D8CFD6C19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OLEContainerScrollView+Swizzling.h"; sourceTree = "<group>"; };
5D8CFD6D19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OLEContainerScrollView+Swizzling.m"; sourceTree = "<group>"; };
5D8CFD7219815DA800B3B7F7 /* OLESwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OLESwizzling.h; sourceTree = "<group>"; };
5D8CFD7319815DA800B3B7F7 /* OLESwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OLESwizzling.m; sourceTree = "<group>"; };
5D8CFD7519815E5300B3B7F7 /* OLEContainerScrollViewContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OLEContainerScrollViewContentView.h; sourceTree = "<group>"; };
5D8CFD7619815E5300B3B7F7 /* OLEContainerScrollViewContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OLEContainerScrollViewContentView.m; sourceTree = "<group>"; };
5D8CFD781981623200B3B7F7 /* OLEContainerScrollView_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OLEContainerScrollView_Private.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -219,6 +225,10 @@
5D8CFD781981623200B3B7F7 /* OLEContainerScrollView_Private.h */,
5D8CFD7519815E5300B3B7F7 /* OLEContainerScrollViewContentView.h */,
5D8CFD7619815E5300B3B7F7 /* OLEContainerScrollViewContentView.m */,
5D8CFD6C19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.h */,
5D8CFD6D19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.m */,
5D8CFD7219815DA800B3B7F7 /* OLESwizzling.h */,
5D8CFD7319815DA800B3B7F7 /* OLESwizzling.m */,
);
path = OLEContainerScrollView;
sourceTree = "<group>";
Expand Down Expand Up @@ -328,6 +338,8 @@
5DBDF1F518D5F26B005B1E18 /* AppDelegate.m in Sources */,
5D8CFD7719815E5300B3B7F7 /* OLEContainerScrollViewContentView.m in Sources */,
5D63B223192250990064DB38 /* NaiveContainerScrollViewController.m in Sources */,
5D8CFD7419815DA800B3B7F7 /* OLESwizzling.m in Sources */,
5D8CFD6E19815CE500B3B7F7 /* OLEContainerScrollView+Swizzling.m in Sources */,
5D050BE71959DEAD00D2447F /* UIColor+RandomColor.m in Sources */,
5DBDF21B18D5F2A2005B1E18 /* OLESimulatedTableView.m in Sources */,
5D2A88EB192246D700CEE791 /* SimpleTableViewController.m in Sources */,
Expand Down

0 comments on commit 2db7ddb

Please sign in to comment.