diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a044ff4861..136a89cb43 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -461,6 +461,10 @@ DE040EF91C2B40AC004692FF /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; }; + DEC447B51C2B9DBC00C8CBD1 /* ASDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DEC447B31C2B9DBC00C8CBD1 /* ASDelegateProxy.h */; }; + DEC447B61C2B9DBC00C8CBD1 /* ASDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = DEC447B31C2B9DBC00C8CBD1 /* ASDelegateProxy.h */; }; + DEC447B71C2B9DBC00C8CBD1 /* ASDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = DEC447B41C2B9DBC00C8CBD1 /* ASDelegateProxy.m */; }; + DEC447B81C2B9DBC00C8CBD1 /* ASDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = DEC447B41C2B9DBC00C8CBD1 /* ASDelegateProxy.m */; }; DECBD6E71BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; @@ -754,6 +758,8 @@ D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = ""; }; + DEC447B31C2B9DBC00C8CBD1 /* ASDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDelegateProxy.h; sourceTree = ""; }; + DEC447B41C2B9DBC00C8CBD1 /* ASDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDelegateProxy.m; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1149,6 +1155,8 @@ 25B171EA1C12242700508A7A /* Data Controller */ = { isa = PBXGroup; children = ( + DEC447B31C2B9DBC00C8CBD1 /* ASDelegateProxy.h */, + DEC447B41C2B9DBC00C8CBD1 /* ASDelegateProxy.m */, 251B8EF21BBB3D690087C538 /* ASCollectionDataController.h */, 251B8EF31BBB3D690087C538 /* ASCollectionDataController.mm */, 464052191A3F83C40061C0BA /* ASDataController.h */, @@ -1264,6 +1272,7 @@ 18C2ED7E1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, 257754C01BEE458E00737CA5 /* ASTextNodeWordKerner.h in Headers */, AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */, + DEC447B51C2B9DBC00C8CBD1 /* ASDelegateProxy.h in Headers */, 205F0E1D1B373A2C007741D0 /* ASCollectionViewLayoutController.h in Headers */, AC3C4A541A113EEC00143C57 /* ASCollectionViewProtocols.h in Headers */, 058D0A49195D05CB00B7D73C /* ASControlNode+Subclasses.h in Headers */, @@ -1432,6 +1441,7 @@ 34EFC7791B701D3600AD841F /* ASLayoutSpecUtilities.h in Headers */, B350625C1B010F070018CF92 /* ASLog.h in Headers */, 0442850E1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.h in Headers */, + DEC447B61C2B9DBC00C8CBD1 /* ASDelegateProxy.h in Headers */, B35062041B010EFD0018CF92 /* ASMultiplexImageNode.h in Headers */, DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */, B35062241B010EFD0018CF92 /* ASMutableAttributedStringBuilder.h in Headers */, @@ -1687,6 +1697,7 @@ 0549634A1A1EA066000F8E56 /* ASBasicImageDownloader.mm in Sources */, 299DA1AA1A828D2900162D41 /* ASBatchContext.mm in Sources */, AC6456091B0A335000CF11B8 /* ASCellNode.m in Sources */, + DEC447B71C2B9DBC00C8CBD1 /* ASDelegateProxy.m in Sources */, ACF6ED1D1B17843500DA7C62 /* ASCenterLayoutSpec.mm in Sources */, 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.m in Sources */, 92DD2FE41BF4B97E0074C9DD /* ASMapNode.mm in Sources */, @@ -1816,6 +1827,7 @@ 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, + DEC447B81C2B9DBC00C8CBD1 /* ASDelegateProxy.m in Sources */, B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.m in Sources */, diff --git a/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata b/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata index 574f0ec195..5f7400170b 100644 --- a/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata +++ b/AsyncDisplayKit.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,12 @@ + + + + diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 5d3d051bc4..73e61653f5 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -8,6 +8,7 @@ #import "ASAssert.h" #import "ASBatchFetching.h" +#import "ASDelegateProxy.h" #import "ASCollectionView.h" #import "ASCollectionNode.h" #import "ASCollectionDataController.h" @@ -22,87 +23,6 @@ static const NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimation static const ASSizeRange kInvalidSizeRange = {CGSizeZero, CGSizeZero}; static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; -#pragma mark - -#pragma mark Proxying. - -/** - * ASCollectionView intercepts and/or overrides a few of UICollectionView's critical data source and delegate methods. - * - * Any selector included in this function *MUST* be implemented by ASCollectionView. - */ -static BOOL _isInterceptedSelector(SEL sel) -{ - return ( - // handled by ASCollectionView node<->cell machinery - sel == @selector(collectionView:cellForItemAtIndexPath:) || - sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || - sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || - - // handled by ASRangeController - sel == @selector(numberOfSectionsInCollectionView:) || - sel == @selector(collectionView:numberOfItemsInSection:) || - - // used for ASRangeController visibility updates - sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || - sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || - - // used for batch fetching API - sel == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) - ); -} - - -/** - * Stand-in for UICollectionViewDataSource and UICollectionViewDelegate. Any method calls we intercept are routed to ASCollectionView; - * everything else leaves AsyncDisplayKit safely and arrives at the original intended data source and delegate. - */ -@interface _ASCollectionViewProxy : NSProxy -- (instancetype)initWithTarget:(id)target interceptor:(ASCollectionView *)interceptor; -@end - -@implementation _ASCollectionViewProxy { - id __weak _target; - ASCollectionView * __weak _interceptor; -} - -- (instancetype)initWithTarget:(id)target interceptor:(ASCollectionView *)interceptor -{ - // -[NSProxy init] is undefined - if (!self) { - return nil; - } - - ASDisplayNodeAssert(target, @"target must not be nil"); - ASDisplayNodeAssert(interceptor, @"interceptor must not be nil"); - - _target = target; - _interceptor = interceptor; - - return self; -} - -- (BOOL)respondsToSelector:(SEL)aSelector -{ - ASDisplayNodeAssert(_target, @"target must not be nil"); // catch weak ref's being nilled early - ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); - - return (_isInterceptedSelector(aSelector) || [_target respondsToSelector:aSelector]); -} - -- (id)forwardingTargetForSelector:(SEL)aSelector -{ - ASDisplayNodeAssert(_target, @"target must not be nil"); // catch weak ref's being nilled early - ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); - - if (_isInterceptedSelector(aSelector)) { - return _interceptor; - } - - return [_target respondsToSelector:aSelector] ? _target : nil; -} - -@end - #pragma mark - #pragma mark ASCellNode<->UICollectionViewCell bridging. @@ -138,9 +58,9 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark - #pragma mark ASCollectionView. -@interface ASCollectionView () { - _ASCollectionViewProxy *_proxyDataSource; - _ASCollectionViewProxy *_proxyDelegate; +@interface ASCollectionView () { + ASCollectionViewProxy *_proxyDataSource; + ASCollectionViewProxy *_proxyDelegate; ASCollectionDataController *_dataController; ASRangeController *_rangeController; @@ -155,6 +75,7 @@ static BOOL _isInterceptedSelector(SEL sel) BOOL _collectionViewLayoutImplementsInsetSection; BOOL _asyncDataSourceImplementsConstrainedSizeForNode; BOOL _queuedNodeSizeUpdate; + BOOL _isDeallocating; ASBatchContext *_batchContext; @@ -244,6 +165,12 @@ static BOOL _isInterceptedSelector(SEL sel) _layoutInspector = [self flowLayoutInspector]; } + _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + super.delegate = (id)_proxyDelegate; + + _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; + super.dataSource = (id)_proxyDataSource; + _registeredSupplementaryKinds = [NSMutableSet set]; self.backgroundColor = [UIColor whiteColor]; @@ -255,10 +182,10 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)dealloc { - // Sometimes the UIKit classes can call back to their delegate even during deallocation. - // This bug might be iOS 7-specific. - super.delegate = nil; - super.dataSource = nil; + // Sometimes the UIKit classes can call back to their delegate even during deallocation, due to animation completion blocks etc. + _isDeallocating = YES; + [self setAsyncDelegate:nil]; + [self setAsyncDataSource:nil]; } /** @@ -312,24 +239,35 @@ static BOOL _isInterceptedSelector(SEL sel) ASDisplayNodeAssert(delegate == nil, @"ASCollectionView uses asyncDelegate, not UICollectionView's delegate property."); } +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy +{ + if (proxy == _proxyDelegate) { + [self setAsyncDelegate:nil]; + } else if (proxy == _proxyDataSource) { + [self setAsyncDataSource:nil]; + } +} + - (void)setAsyncDataSource:(id)asyncDataSource { // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to nil out - // super.dataSource in this case because calls to _ASTableViewProxy will start failing and cause crashes. + // super.dataSource in this case because calls to ASCollectionViewProxy will start failing and cause crashes. + + super.dataSource = nil; if (asyncDataSource == nil) { - super.dataSource = nil; _asyncDataSource = nil; - _proxyDataSource = nil; + _proxyDataSource = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; _asyncDataSourceImplementsConstrainedSizeForNode = NO; } else { _asyncDataSource = asyncDataSource; - _proxyDataSource = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; - super.dataSource = (id)_proxyDataSource; + _proxyDataSource = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; _asyncDataSourceImplementsConstrainedSizeForNode = ([_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] ? 1 : 0); } + + super.dataSource = (id)_proxyDataSource; } - (void)setAsyncDelegate:(id)asyncDelegate @@ -337,22 +275,25 @@ static BOOL _isInterceptedSelector(SEL sel) // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate // will return as nil (ARC magic) even though the _proxyDelegate still exists. It's really important to nil out - // super.delegate in this case because calls to _ASTableViewProxy will start failing and cause crashes. + // super.delegate in this case because calls to ASCollectionViewProxy will start failing and cause crashes. + + // Order is important here, the asyncDelegate must be callable while nilling super.delegate to avoid random crashes + // in UIScrollViewAccessibility. + + super.delegate = nil; if (asyncDelegate == nil) { - // order is important here, the delegate must be callable while nilling super.delegate to avoid random crashes - // in UIScrollViewAccessibility. - super.delegate = nil; _asyncDelegate = nil; - _proxyDelegate = nil; + _proxyDelegate = _isDeallocating ? nil : [[ASCollectionViewProxy alloc] initWithTarget:nil interceptor:self]; _asyncDelegateImplementsInsetSection = NO; } else { _asyncDelegate = asyncDelegate; - _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; - super.delegate = (id)_proxyDelegate; + _proxyDelegate = [[ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; _asyncDelegateImplementsInsetSection = ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)] ? 1 : 0); } + super.delegate = (id)_proxyDelegate; + [_layoutInspector didChangeCollectionViewDelegate:asyncDelegate]; } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index ddb6838037..8d799b5b4a 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -13,6 +13,7 @@ #import "ASBatchFetching.h" #import "ASChangeSetDataController.h" #import "ASCollectionViewLayoutController.h" +#import "ASDelegateProxy.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASInternalHelpers.h" #import "ASLayout.h" @@ -24,87 +25,6 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; //#define LOG(...) NSLog(__VA_ARGS__) #define LOG(...) -#pragma mark - -#pragma mark Proxying. - -/** - * ASTableView intercepts and/or overrides a few of UITableView's critical data source and delegate methods. - * - * Any selector included in this function *MUST* be implemented by ASTableView. - */ -static BOOL _isInterceptedSelector(SEL sel) -{ - return ( - // handled by ASTableView node<->cell machinery - sel == @selector(tableView:cellForRowAtIndexPath:) || - sel == @selector(tableView:heightForRowAtIndexPath:) || - - // handled by ASRangeController - sel == @selector(numberOfSectionsInTableView:) || - sel == @selector(tableView:numberOfRowsInSection:) || - - // used for ASRangeController visibility updates - sel == @selector(tableView:willDisplayCell:forRowAtIndexPath:) || - sel == @selector(tableView:didEndDisplayingCell:forRowAtIndexPath:) || - - // used for batch fetching API - sel == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) - ); -} - - -/** - * Stand-in for UITableViewDataSource and UITableViewDelegate. Any method calls we intercept are routed to ASTableView; - * everything else leaves AsyncDisplayKit safely and arrives at the original intended data source and delegate. - */ -@interface _ASTableViewProxy : NSProxy -- (instancetype)initWithTarget:(id)target interceptor:(ASTableView *)interceptor; -@end - -@implementation _ASTableViewProxy { - id __weak _target; - ASTableView * __weak _interceptor; -} - -- (instancetype)initWithTarget:(id)target interceptor:(ASTableView *)interceptor -{ - // -[NSProxy init] is undefined - if (!self) { - return nil; - } - - ASDisplayNodeAssert(target, @"target must not be nil"); - ASDisplayNodeAssert(interceptor, @"interceptor must not be nil"); - - _target = target; - _interceptor = interceptor; - - return self; -} - -- (BOOL)respondsToSelector:(SEL)aSelector -{ - ASDisplayNodeAssert(_target, @"target must not be nil"); // catch weak ref's being nilled early - ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); - - return (_isInterceptedSelector(aSelector) || [_target respondsToSelector:aSelector]); -} - -- (id)forwardingTargetForSelector:(SEL)aSelector -{ - ASDisplayNodeAssert(_target, @"target must not be nil"); // catch weak ref's being nilled early - ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); - - if (_isInterceptedSelector(aSelector)) { - return _interceptor; - } - - return [_target respondsToSelector:aSelector] ? _target : nil; -} - -@end - - #pragma mark - #pragma mark ASCellNode<->UITableViewCell bridging. @@ -156,13 +76,12 @@ static BOOL _isInterceptedSelector(SEL sel) @end - #pragma mark - #pragma mark ASTableView -@interface ASTableView () { - _ASTableViewProxy *_proxyDataSource; - _ASTableViewProxy *_proxyDelegate; +@interface ASTableView () { + ASTableViewProxy *_proxyDataSource; + ASTableViewProxy *_proxyDelegate; ASFlowLayoutController *_layoutController; @@ -180,6 +99,7 @@ static BOOL _isInterceptedSelector(SEL sel) CGFloat _nodesConstrainedWidth; BOOL _ignoreNodesConstrainedWidthChange; BOOL _queuedNodeHeightUpdate; + BOOL _isDeallocating; } @property (atomic, assign) BOOL asyncDataSourceLocked; @@ -224,6 +144,12 @@ static BOOL _isInterceptedSelector(SEL sel) // If the initial size is 0, expect a size change very soon which is part of the initial configuration // and should not trigger a relayout. _ignoreNodesConstrainedWidthChange = (_nodesConstrainedWidth == 0); + + _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + super.delegate = (id)_proxyDelegate; + + _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; + super.dataSource = (id)_proxyDataSource; [self registerClass:_ASTableViewCell.class forCellReuseIdentifier:kCellReuseIdentifier]; } @@ -265,9 +191,9 @@ static BOOL _isInterceptedSelector(SEL sel) - (void)dealloc { // Sometimes the UIKit classes can call back to their delegate even during deallocation. - // This bug might be iOS 7-specific. - super.delegate = nil; - super.dataSource = nil; + _isDeallocating = YES; + [self setAsyncDelegate:nil]; + [self setAsyncDataSource:nil]; } #pragma mark - @@ -290,17 +216,19 @@ static BOOL _isInterceptedSelector(SEL sel) // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDataSource in the ViewController's dealloc. In this case our _asyncDataSource // will return as nil (ARC magic) even though the _proxyDataSource still exists. It's really important to nil out - // super.dataSource in this case because calls to _ASTableViewProxy will start failing and cause crashes. - + // super.dataSource in this case because calls to ASTableViewProxy will start failing and cause crashes. + + super.dataSource = nil; + if (asyncDataSource == nil) { - super.dataSource = nil; _asyncDataSource = nil; - _proxyDataSource = nil; + _proxyDataSource = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; } else { _asyncDataSource = asyncDataSource; - _proxyDataSource = [[_ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; - super.dataSource = (id)_proxyDataSource; + _proxyDataSource = [[ASTableViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; } + + super.dataSource = (id)_proxyDataSource; } - (void)setAsyncDelegate:(id)asyncDelegate @@ -308,18 +236,30 @@ static BOOL _isInterceptedSelector(SEL sel) // Note: It's common to check if the value hasn't changed and short-circuit but we aren't doing that here to handle // the (common) case of nilling the asyncDelegate in the ViewController's dealloc. In this case our _asyncDelegate // will return as nil (ARC magic) even though the _proxyDelegate still exists. It's really important to nil out - // super.delegate in this case because calls to _ASTableViewProxy will start failing and cause crashes. + // super.delegate in this case because calls to ASTableViewProxy will start failing and cause crashes. + + // Order is important here, the asyncDelegate must be callable while nilling super.delegate to avoid random crashes + // in UIScrollViewAccessibility. + super.delegate = nil; + if (asyncDelegate == nil) { - // order is important here, the delegate must be callable while nilling super.delegate to avoid random crashes - // in UIScrollViewAccessibility. - super.delegate = nil; _asyncDelegate = nil; - _proxyDelegate = nil; + _proxyDelegate = _isDeallocating ? nil : [[ASTableViewProxy alloc] initWithTarget:nil interceptor:self]; } else { _asyncDelegate = asyncDelegate; - _proxyDelegate = [[_ASTableViewProxy alloc] initWithTarget:asyncDelegate interceptor:self]; - super.delegate = (id)_proxyDelegate; + _proxyDelegate = [[ASTableViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; + } + + super.delegate = (id)_proxyDelegate; +} + +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy +{ + if (proxy == _proxyDelegate) { + [self setAsyncDelegate:nil]; + } else if (proxy == _proxyDataSource) { + [self setAsyncDataSource:nil]; } } diff --git a/AsyncDisplayKit/Details/ASDelegateProxy.h b/AsyncDisplayKit/Details/ASDelegateProxy.h new file mode 100644 index 0000000000..ca5ae5da01 --- /dev/null +++ b/AsyncDisplayKit/Details/ASDelegateProxy.h @@ -0,0 +1,51 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +@class ASDelegateProxy; +@protocol ASDelegateProxyInterceptor +@required +// Called if the target object is discovered to be nil if it had been non-nil at init time. +// This happens if the object is deallocated, because the proxy must maintain a weak reference to avoid cycles. +// Though the target object may become nil, the interceptor must not; it is assumed the interceptor owns the proxy. +- (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy; +@end + +/** + * Stand-in for delegates like UITableView or UICollectionView's delegate / dataSource. + * Any selectors flagged by "interceptsSelector" are routed to the interceptor object and are not delivered to the target. + * Everything else leaves AsyncDisplayKit safely and arrives at the original target object. + */ + +@interface ASDelegateProxy : NSProxy + +- (instancetype)initWithTarget:(id)target interceptor:(id )interceptor; + +// This method must be overridden by a subclass. +- (BOOL)interceptsSelector:(SEL)selector; + +@end + +/** + * ASTableView intercepts and/or overrides a few of UITableView's critical data source and delegate methods. + * + * Any selector included in this function *MUST* be implemented by ASTableView. + */ + +@interface ASTableViewProxy : ASDelegateProxy +@end + +/** + * ASCollectionView intercepts and/or overrides a few of UICollectionView's critical data source and delegate methods. + * + * Any selector included in this function *MUST* be implemented by ASCollectionView. + */ + +@interface ASCollectionViewProxy : ASDelegateProxy +@end diff --git a/AsyncDisplayKit/Details/ASDelegateProxy.m b/AsyncDisplayKit/Details/ASDelegateProxy.m new file mode 100644 index 0000000000..baaff1e62c --- /dev/null +++ b/AsyncDisplayKit/Details/ASDelegateProxy.m @@ -0,0 +1,117 @@ +/* Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "ASDelegateProxy.h" +#import "ASTableView.h" +#import "ASCollectionView.h" +#import "ASAssert.h" + +@implementation ASTableViewProxy + +- (BOOL)interceptsSelector:(SEL)selector +{ + return ( + // handled by ASTableView node<->cell machinery + selector == @selector(tableView:cellForRowAtIndexPath:) || + selector == @selector(tableView:heightForRowAtIndexPath:) || + + // handled by ASRangeController + selector == @selector(numberOfSectionsInTableView:) || + selector == @selector(tableView:numberOfRowsInSection:) || + + // used for ASRangeController visibility updates + selector == @selector(tableView:willDisplayCell:forRowAtIndexPath:) || + selector == @selector(tableView:didEndDisplayingCell:forRowAtIndexPath:) || + + // used for batch fetching API + selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) + ); +} + +@end + +@implementation ASCollectionViewProxy + +- (BOOL)interceptsSelector:(SEL)selector +{ + return ( + // handled by ASCollectionView node<->cell machinery + selector == @selector(collectionView:cellForItemAtIndexPath:) || + selector == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + selector == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || + + // handled by ASRangeController + selector == @selector(numberOfSectionsInCollectionView:) || + selector == @selector(collectionView:numberOfItemsInSection:) || + + // used for ASRangeController visibility updates + selector == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || + selector == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || + + // used for batch fetching API + selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) + ); +} + +@end + +@implementation ASDelegateProxy { + id __weak _target; + id __weak _interceptor; +} + +- (instancetype)initWithTarget:(id )target interceptor:(id )interceptor +{ + // -[NSProxy init] is undefined + if (!self) { + return nil; + } + + ASDisplayNodeAssert(interceptor, @"interceptor must not be nil"); + + _target = target ? : [NSNull null]; + _interceptor = interceptor; + + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); + + if ([self interceptsSelector:aSelector]) { + return YES; + } else { + // Also return NO if _target has become nil due to zeroing weak reference (or placeholder initialization). + return [_target respondsToSelector:aSelector]; + } +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + ASDisplayNodeAssert(_interceptor, @"interceptor must not be nil"); + + if ([self interceptsSelector:aSelector]) { + return _interceptor; + } else { + if (_target) { + return [_target respondsToSelector:aSelector] ? _target : nil; + } else { + [_interceptor proxyTargetHasDeallocated:self]; + return nil; + } + } +} + +- (BOOL)interceptsSelector:(SEL)selector +{ + ASDisplayNodeAssert(NO, @"This method must be overridden by subclasses."); + return NO; +} + +@end diff --git a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m index 11271b23fd..9def4240a1 100644 --- a/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m +++ b/AsyncDisplayKitTests/ASBasicImageDownloaderTests.m @@ -11,45 +11,38 @@ #import // Z in the name to delay running until after the test instance is operating normally. -@interface ASZBasicImageDownloaderTests : XCTestCase +@interface ASBasicImageDownloaderTests : XCTestCase @end -@implementation ASZBasicImageDownloaderTests +@implementation ASBasicImageDownloaderTests - (void)testAsynchronouslyDownloadTheSameURLTwice { ASBasicImageDownloader *downloader = [ASBasicImageDownloader sharedImageDownloader]; NSURL *URL = [NSURL URLWithString:@"http://wrongPath/wrongResource.png"]; - - dispatch_group_t group = dispatch_group_create(); - + __block BOOL firstDone = NO; - dispatch_group_enter(group); [downloader downloadImageWithURL:URL callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) downloadProgressBlock:nil completion:^(CGImageRef image, NSError *error) { firstDone = YES; - dispatch_group_leave(group); }]; __block BOOL secondDone = NO; - dispatch_group_enter(group); [downloader downloadImageWithURL:URL callbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) downloadProgressBlock:nil completion:^(CGImageRef image, NSError *error) { secondDone = YES; - dispatch_group_leave(group); }]; - - XCTAssert(0 == dispatch_group_wait(group, dispatch_time(0, 10 * 1000000000)), @"URL loading takes too long"); - - XCTAssert(firstDone && secondDone, @"Not all handlers has been called"); + + sleep(3); + XCTAssert(firstDone && secondDone, @"Not all ASBasicImageDownloader completion handlers have been called after 3 seconds"); } @end diff --git a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj index 56aef5f7f6..3cdc925105 100644 --- a/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/ASCollectionView/Sample.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; FABD6D156A3EB118497E5CE6 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F02BAF78E68BC56FD8C161B7 /* libPods.a */; }; + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -34,6 +35,8 @@ AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; CD1ABB23007FEDB31D8C1978 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; F02BAF78E68BC56FD8C161B7 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PresentingViewController.h; sourceTree = ""; }; + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PresentingViewController.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -82,6 +85,8 @@ AC3C4A661A11F47200143C57 /* AppDelegate.m */, AC3C4A681A11F47200143C57 /* ViewController.h */, AC3C4A691A11F47200143C57 /* ViewController.m */, + FC3FCA7E1C2B1564009F6D6D /* PresentingViewController.h */, + FC3FCA7F1C2B1564009F6D6D /* PresentingViewController.m */, AC3C4A8D1A11F80C00143C57 /* Images.xcassets */, AC3C4A611A11F47200143C57 /* Supporting Files */, 9B92C87F1BC17D3000EE46B2 /* SupplementaryNode.h */, @@ -125,7 +130,6 @@ AC3C4A5B1A11F47200143C57 /* Frameworks */, AC3C4A5C1A11F47200143C57 /* Resources */, A6902C454C7661D0D277AC62 /* Copy Pods Resources */, - EC37EEC9933F5786936BFE7C /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -196,21 +200,6 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; showEnvVarsInLog = 0; }; - EC37EEC9933F5786936BFE7C /* Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; F868CFBB21824CC9521B6588 /* Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -236,6 +225,7 @@ 25FDEC921BF31EE700CEB123 /* ItemNode.m in Sources */, AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */, 9B92C8811BC17D3000EE46B2 /* SupplementaryNode.m in Sources */, + FC3FCA801C2B1564009F6D6D /* PresentingViewController.m in Sources */, AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */, AC3C4A641A11F47200143C57 /* main.m in Sources */, ); diff --git a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/ASCollectionView/Sample/AppDelegate.h b/examples/ASCollectionView/Sample/AppDelegate.h index 2aa29369b4..80b7fb3d50 100644 --- a/examples/ASCollectionView/Sample/AppDelegate.h +++ b/examples/ASCollectionView/Sample/AppDelegate.h @@ -11,6 +11,8 @@ #import +#define SIMULATE_WEB_RESPONSE 0 + @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; diff --git a/examples/ASCollectionView/Sample/AppDelegate.m b/examples/ASCollectionView/Sample/AppDelegate.m index a979170a63..19ceedd960 100644 --- a/examples/ASCollectionView/Sample/AppDelegate.m +++ b/examples/ASCollectionView/Sample/AppDelegate.m @@ -11,6 +11,7 @@ #import "AppDelegate.h" +#import "PresentingViewController.h" #import "ViewController.h" @implementation AppDelegate @@ -31,10 +32,14 @@ - (void)pushNewViewControllerAnimated:(BOOL)animated { UINavigationController *navController = (UINavigationController *)self.window.rootViewController; - + +#if SIMULATE_WEB_RESPONSE + UIViewController *viewController = [[PresentingViewController alloc] init]; +#else UIViewController *viewController = [[ViewController alloc] init]; viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Another Copy" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; - +#endif + [navController pushViewController:viewController animated:animated]; } diff --git a/examples/ASCollectionView/Sample/PresentingViewController.h b/examples/ASCollectionView/Sample/PresentingViewController.h new file mode 100644 index 0000000000..bd2308fab3 --- /dev/null +++ b/examples/ASCollectionView/Sample/PresentingViewController.h @@ -0,0 +1,13 @@ +// +// PresentingViewController.h +// Sample +// +// Created by Tom King on 12/23/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import + +@interface PresentingViewController : UIViewController + +@end diff --git a/examples/ASCollectionView/Sample/PresentingViewController.m b/examples/ASCollectionView/Sample/PresentingViewController.m new file mode 100644 index 0000000000..49c65e6906 --- /dev/null +++ b/examples/ASCollectionView/Sample/PresentingViewController.m @@ -0,0 +1,30 @@ +// +// PresentingViewController.m +// Sample +// +// Created by Tom King on 12/23/15. +// Copyright © 2015 Facebook. All rights reserved. +// + +#import "PresentingViewController.h" +#import "ViewController.h" + +@interface PresentingViewController () + +@end + +@implementation PresentingViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Push Details" style:UIBarButtonItemStylePlain target:self action:@selector(pushNewViewController)]; +} + +- (void)pushNewViewController +{ + ViewController *controller = [[ViewController alloc] init]; + [self.navigationController pushViewController:controller animated:true]; +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index a9d3e12a7b..00aea567dc 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -18,6 +18,7 @@ @interface ViewController () { ASCollectionView *_collectionView; + NSArray *_data; } @end @@ -37,7 +38,7 @@ layout.headerReferenceSize = CGSizeMake(50.0, 50.0); layout.footerReferenceSize = CGSizeMake(50.0, 50.0); - _collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:YES]; + _collectionView = [[ASCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.asyncDataSource = self; _collectionView.asyncDelegate = self; _collectionView.backgroundColor = [UIColor whiteColor]; @@ -45,8 +46,10 @@ [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionHeader]; [_collectionView registerSupplementaryNodeOfKind:UICollectionElementKindSectionFooter]; +#if !SIMULATE_WEB_RESPONSE self.navigationItem.leftItemsSupplementBackButton = YES; self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reloadTapped)]; +#endif return self; } @@ -56,6 +59,31 @@ [super viewDidLoad]; [self.view addSubview:_collectionView]; + +#if SIMULATE_WEB_RESPONSE + __weak typeof(self) weakSelf = self; + void(^mockWebService)() = ^{ + NSLog(@"ViewController \"got data from a web service\""); + ViewController *strongSelf = weakSelf; + if (strongSelf != nil) + { + NSLog(@"ViewController is not nil"); + strongSelf->_data = [[NSArray alloc] init]; + [strongSelf->_collectionView performBatchUpdates:^{ + [strongSelf->_collectionView insertSections:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, 100)]]; + } completion:nil]; + NSLog(@"ViewController finished updating collectionView"); + } + else { + NSLog(@"ViewController is nil - won't update collectionView"); + } + }; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), mockWebService); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.navigationController popViewControllerAnimated:YES]; + }); +#endif } - (void)viewWillLayoutSubviews @@ -101,7 +129,11 @@ - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { +#if SIMULATE_WEB_RESPONSE + return _data == nil ? 0 : 100; +#else return 100; +#endif } - (void)collectionViewLockDataSource:(ASCollectionView *)collectionView @@ -125,4 +157,11 @@ return UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); } +#if SIMULATE_WEB_RESPONSE +-(void)dealloc +{ + NSLog(@"ViewController is deallocing"); +} +#endif + @end