diff --git a/.travis.yml b/.travis.yml index c714a87dae..8899c440c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: objective-c before_install: - brew update - brew reinstall xctool - - gem update cocoapods + - gem install cocoapods -v 0.37.2 - gem install slather - xcrun simctl list install: echo "<3" diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c0bd2307e6..a12b858199 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -139,6 +139,10 @@ 05A6D05B19D0EB64002DD95E /* ASDealloc2MainObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */; }; 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 18C2ED7E1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.m */; }; + 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.m */; }; 1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */; }; 204C979E1B362CB3002B1083 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 204C979D1B362CB3002B1083 /* Default-568h@2x.png */; }; 205F0E0F1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -166,9 +170,40 @@ 299DA1A91A828D2900162D41 /* ASBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 299DA1A71A828D2900162D41 /* ASBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 299DA1AA1A828D2900162D41 /* ASBatchContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 299DA1A81A828D2900162D41 /* ASBatchContext.mm */; }; 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */; }; + 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED071B17843500DA7C62 /* ASDimension.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED081B17843500DA7C62 /* ASDimension.mm */; }; + 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 34EFC75E1B701BF000AD841F /* ASInternalHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.mm */; }; + 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */; }; + 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */; }; + 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED031B17843500DA7C62 /* ASCenterLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */; }; + 34EFC7651B701CCC00AD841F /* ASRelativeSize.h in Headers */ = {isa = PBXBuildFile; fileRef = AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7661B701CD200AD841F /* ASRelativeSize.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */; }; + 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED0B1B17843500DA7C62 /* ASLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */; }; + 34EFC7691B701CE100AD841F /* ASLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED111B17843500DA7C62 /* ASLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76A1B701CE600AD841F /* ASLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED0D1B17843500DA7C62 /* ASLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED0E1B17843500DA7C62 /* ASLayoutSpec.mm */; }; + 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED121B17843500DA7C62 /* ASOverlayLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED131B17843500DA7C62 /* ASOverlayLayoutSpec.mm */; }; + 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED141B17843500DA7C62 /* ASRatioLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED151B17843500DA7C62 /* ASRatioLayoutSpec.mm */; }; + 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */; }; + 34EFC7731B701D0700AD841F /* ASStaticLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED181B17843500DA7C62 /* ASStaticLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED191B17843500DA7C62 /* ASStaticLayoutSpec.mm */; }; + 34EFC7751B701D2400AD841F /* ASStackPositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED471B17847A00DA7C62 /* ASStackPositionedLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 34EFC7761B701D2A00AD841F /* ASStackPositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED481B17847A00DA7C62 /* ASStackPositionedLayout.mm */; }; + 34EFC7771B701D2D00AD841F /* ASStackUnpositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED491B17847A00DA7C62 /* ASStackUnpositionedLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED4A1B17847A00DA7C62 /* ASStackUnpositionedLayout.mm */; }; + 34EFC7791B701D3600AD841F /* ASLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED451B17847A00DA7C62 /* ASLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - 430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */; }; - 430E7C901B4C23F100697A4C /* ASIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */; }; + 430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 430E7C901B4C23F100697A4C /* ASIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 430E7C8D1B4C23F100697A4C /* ASIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 430E7C911B4C23F100697A4C /* ASIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 430E7C8E1B4C23F100697A4C /* ASIndexPath.m */; }; 430E7C921B4C23F100697A4C /* ASIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 430E7C8E1B4C23F100697A4C /* ASIndexPath.m */; }; 464052201A3F83C40061C0BA /* ASDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = 464052191A3F83C40061C0BA /* ASDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -186,14 +221,32 @@ 509E68651B3AEDC5009B9150 /* CGRect+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CGRect+ASConvenience.h */; }; 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CGRect+ASConvenience.m */; }; 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C5FA3511B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C5FA3521B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */; }; + 9C5FA3541B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */; }; + 9C5FA35F1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5FA35C1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm */; }; + 9C5FA3601B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5FA35C1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm */; }; + 9C65A72A1BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9C65A72B1BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C6BB3B01B8CC9C200F13F52 /* ASStaticLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C6BB3B31B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C6BB3B01B8CC9C200F13F52 /* ASStaticLayoutable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9C8221951BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */; settings = {ASSET_TAGS = (); }; }; + 9C8221961BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */; settings = {ASSET_TAGS = (); }; }; + 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */; settings = {ASSET_TAGS = (); }; }; + 9C8221981BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */; settings = {ASSET_TAGS = (); }; }; + 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; - AC21EC101B3D0BF600C8B19A /* ASStackLayoutChild.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutChild.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC3C4A521A1139C100143C57 /* ASCollectionView.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A501A1139C100143C57 /* ASCollectionView.mm */; }; AC3C4A541A113EEC00143C57 /* ASCollectionViewProtocols.h in Headers */ = {isa = PBXBuildFile; fileRef = AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC47D9421B3B891B00AAEE9D /* ASCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = AC6456071B0A335000CF11B8 /* ASCellNode.m */; }; - AC47D9451B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.h in Headers */ = {isa = PBXBuildFile; fileRef = AC47D9431B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.h */; settings = {ATTRIBUTES = (Public, ); }; }; - AC47D9461B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC47D9441B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.mm */; }; + AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */ = {isa = PBXBuildFile; fileRef = AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */; }; AC6456091B0A335000CF11B8 /* ASCellNode.m in Sources */ = {isa = PBXBuildFile; fileRef = AC6456071B0A335000CF11B8 /* ASCellNode.m */; }; ACF6ED1A1B17843500DA7C62 /* ASBackgroundLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; ACF6ED1B1B17843500DA7C62 /* ASBackgroundLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */; }; @@ -414,7 +467,7 @@ 058D09D5195D050800B7D73C /* ASControlNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASControlNode.h; sourceTree = ""; }; 058D09D6195D050800B7D73C /* ASControlNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASControlNode.m; sourceTree = ""; }; 058D09D7195D050800B7D73C /* ASControlNode+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+Subclasses.h"; sourceTree = ""; }; - 058D09D8195D050800B7D73C /* ASDisplayNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDisplayNode.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 058D09D8195D050800B7D73C /* ASDisplayNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDisplayNode.h; sourceTree = ""; }; 058D09D9195D050800B7D73C /* ASDisplayNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDisplayNode.mm; sourceTree = ""; }; 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = "ASDisplayNode+Subclasses.h"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeExtras.h; sourceTree = ""; }; @@ -422,7 +475,7 @@ 058D09DD195D050800B7D73C /* ASImageNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageNode.h; sourceTree = ""; }; 058D09DE195D050800B7D73C /* ASImageNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASImageNode.mm; sourceTree = ""; }; 058D09DF195D050800B7D73C /* ASTextNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextNode.h; sourceTree = ""; }; - 058D09E0195D050800B7D73C /* ASTextNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTextNode.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 058D09E0195D050800B7D73C /* ASTextNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTextNode.mm; sourceTree = ""; }; 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayLayer.h; sourceTree = ""; }; 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASDisplayLayer.mm; sourceTree = ""; }; 058D09E4195D050800B7D73C /* _ASDisplayView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayView.h; sourceTree = ""; }; @@ -487,6 +540,8 @@ 05A6D05919D0EB64002DD95E /* ASDealloc2MainObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASDealloc2MainObject.m; path = ../Details/ASDealloc2MainObject.m; sourceTree = ""; }; 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASSnapshotTestCase.mm; sourceTree = ""; }; 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; + 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; + 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionNode.m; sourceTree = ""; }; 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; 204C979D1B362CB3002B1083 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewLayout+ASConvenience.h"; sourceTree = ""; }; @@ -525,13 +580,22 @@ 4640521E1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMultidimensionalArrayUtils.h; sourceTree = ""; }; 4640521F1A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMultidimensionalArrayUtils.mm; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; + 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutable.h; path = AsyncDisplayKit/Layout/ASStackLayoutable.h; sourceTree = ""; }; + 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutOptions.h; path = AsyncDisplayKit/Layout/ASLayoutOptions.h; sourceTree = ""; }; + 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutOptions.mm; path = AsyncDisplayKit/Layout/ASLayoutOptions.mm; sourceTree = ""; }; + 9C5FA35C1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASLayoutOptionsPrivate.mm; path = AsyncDisplayKit/Layout/ASLayoutOptionsPrivate.mm; sourceTree = ""; }; + 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutOptionsPrivate.h; sourceTree = ""; }; + 9C6BB3B01B8CC9C200F13F52 /* ASStaticLayoutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStaticLayoutable.h; path = AsyncDisplayKit/Layout/ASStaticLayoutable.h; sourceTree = ""; }; + 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackBaselinePositionedLayout.h; sourceTree = ""; }; + 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackBaselinePositionedLayout.mm; sourceTree = ""; }; + 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutablePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutablePrivate.h; sourceTree = ""; }; 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewTests.m; sourceTree = ""; }; - AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutChild.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutChild.h; path = AsyncDisplayKit/Layout/ASStackLayoutChild.h; sourceTree = ""; }; + AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = ""; }; AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionView.h; sourceTree = ""; }; AC3C4A501A1139C100143C57 /* ASCollectionView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionView.mm; sourceTree = ""; }; AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewProtocols.h; sourceTree = ""; }; - AC47D9431B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStaticLayoutSpecDimension.h; path = AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.h; sourceTree = ""; }; - AC47D9441B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASStaticLayoutSpecDimension.mm; path = AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.mm; sourceTree = ""; }; + AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRelativeSize.h; path = AsyncDisplayKit/Layout/ASRelativeSize.h; sourceTree = ""; }; + AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRelativeSize.mm; path = AsyncDisplayKit/Layout/ASRelativeSize.mm; sourceTree = ""; }; AC6456071B0A335000CF11B8 /* ASCellNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCellNode.m; sourceTree = ""; }; ACF6ED011B17843500DA7C62 /* ASBackgroundLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASBackgroundLayoutSpec.h; path = AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h; sourceTree = ""; }; ACF6ED021B17843500DA7C62 /* ASBackgroundLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = ASBackgroundLayoutSpec.mm; path = AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm; sourceTree = ""; }; @@ -571,8 +635,8 @@ ACF6ED5A1B178DC700DA7C62 /* ASRatioLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRatioLayoutSpecSnapshotTests.mm; sourceTree = ""; }; ACF6ED5B1B178DC700DA7C62 /* ASStackLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASStackLayoutSpecSnapshotTests.mm; sourceTree = ""; }; B35061DA1B010EDF0018CF92 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B35061DD1B010EDF0018CF92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B35061DE1B010EDF0018CF92 /* AsyncDisplayKit-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit-iOS.h"; sourceTree = ""; }; + B35061DD1B010EDF0018CF92 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "../AsyncDisplayKit-iOS/Info.plist"; sourceTree = ""; }; + B35061DE1B010EDF0018CF92 /* AsyncDisplayKit-iOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AsyncDisplayKit-iOS.h"; path = "../AsyncDisplayKit-iOS/AsyncDisplayKit-iOS.h"; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; 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 = ""; }; @@ -656,7 +720,9 @@ 058D09AD195D04C000B7D73C /* Products */, FD40E2760492F0CAAEAD552D /* Pods */, ); + indentWidth = 2; sourceTree = ""; + tabWidth = 2; }; 058D09AD195D04C000B7D73C /* Products */ = { isa = PBXGroup; @@ -687,6 +753,8 @@ children = ( 055F1A3A19ABD43F004DAFF1 /* ASCellNode.h */, AC6456071B0A335000CF11B8 /* ASCellNode.m */, + 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */, + 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.m */, AC3C4A4F1A1139C100143C57 /* ASCollectionView.h */, AC3C4A501A1139C100143C57 /* ASCollectionView.mm */, AC3C4A531A113EEC00143C57 /* ASCollectionViewProtocols.h */, @@ -862,6 +930,9 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + 9C65A7291BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h */, + 9C8221931BA237B80037F19A /* ASStackBaselinePositionedLayout.h */, + 9C8221941BA237B80037F19A /* ASStackBaselinePositionedLayout.mm */, 296A0A2C1A9516B2005ACEAA /* ASBatchFetching.h */, 2967F9E11AB0A4CF0072E4AB /* ASBasicImageDownloaderInternal.h */, 296A0A2D1A9516B2005ACEAA /* ASBatchFetching.m */, @@ -914,24 +985,30 @@ ACF6ED041B17843500DA7C62 /* ASCenterLayoutSpec.mm */, ACF6ED071B17843500DA7C62 /* ASDimension.h */, ACF6ED081B17843500DA7C62 /* ASDimension.mm */, + AC47D9431B3BB41900AAEE9D /* ASRelativeSize.h */, + AC47D9441B3BB41900AAEE9D /* ASRelativeSize.mm */, ACF6ED091B17843500DA7C62 /* ASInsetLayoutSpec.h */, ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */, ACF6ED0B1B17843500DA7C62 /* ASLayout.h */, ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */, ACF6ED111B17843500DA7C62 /* ASLayoutable.h */, + 9CDC18CB1B910E12004965E2 /* ASLayoutablePrivate.h */, ACF6ED0D1B17843500DA7C62 /* ASLayoutSpec.h */, ACF6ED0E1B17843500DA7C62 /* ASLayoutSpec.mm */, ACF6ED121B17843500DA7C62 /* ASOverlayLayoutSpec.h */, ACF6ED131B17843500DA7C62 /* ASOverlayLayoutSpec.mm */, ACF6ED141B17843500DA7C62 /* ASRatioLayoutSpec.h */, ACF6ED151B17843500DA7C62 /* ASRatioLayoutSpec.mm */, - AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutChild.h */, + 9C49C36E1B853957000B0DD5 /* ASStackLayoutable.h */, + AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */, ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */, ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */, + 9C6BB3B01B8CC9C200F13F52 /* ASStaticLayoutable.h */, ACF6ED181B17843500DA7C62 /* ASStaticLayoutSpec.h */, ACF6ED191B17843500DA7C62 /* ASStaticLayoutSpec.mm */, - AC47D9431B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.h */, - AC47D9441B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.mm */, + 9C5FA34F1B8F6ADF00A62714 /* ASLayoutOptions.h */, + 9C5FA3501B8F6ADF00A62714 /* ASLayoutOptions.mm */, + 9C5FA35C1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm */, ); name = Layout; path = ..; @@ -971,8 +1048,9 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - AC21EC101B3D0BF600C8B19A /* ASStackLayoutChild.h in Headers */, - AC47D9451B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.h in Headers */, + AC21EC101B3D0BF600C8B19A /* ASStackLayoutDefines.h in Headers */, + AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */, + 9C49C36F1B853957000B0DD5 /* ASStackLayoutable.h in Headers */, ACF6ED511B17847A00DA7C62 /* ASStackUnpositionedLayout.h in Headers */, ACF6ED2D1B17843500DA7C62 /* ASRatioLayoutSpec.h in Headers */, ACF6ED261B17843500DA7C62 /* ASLayoutSpec.h in Headers */, @@ -980,22 +1058,26 @@ ACF6ED201B17843500DA7C62 /* ASDimension.h in Headers */, ACF6ED2B1B17843500DA7C62 /* ASOverlayLayoutSpec.h in Headers */, ACF6ED1C1B17843500DA7C62 /* ASCenterLayoutSpec.h in Headers */, + 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, ACF6ED2A1B17843500DA7C62 /* ASLayoutable.h in Headers */, ACF6ED311B17843500DA7C62 /* ASStaticLayoutSpec.h in Headers */, ACF6ED241B17843500DA7C62 /* ASLayout.h in Headers */, ACF6ED2F1B17843500DA7C62 /* ASStackLayoutSpec.h in Headers */, + 9C65A72A1BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h in Headers */, ACF6ED1A1B17843500DA7C62 /* ASBackgroundLayoutSpec.h in Headers */, 291B63FB1AA53A7A000A71B3 /* ASScrollDirection.h in Headers */, 464052221A3F83C40061C0BA /* ASFlowLayoutController.h in Headers */, 464052241A3F83C40061C0BA /* ASLayoutController.h in Headers */, 464052201A3F83C40061C0BA /* ASDataController.h in Headers */, 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */, + 9C8221951BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, 058D0A47195D05CB00B7D73C /* ASControlNode.h in Headers */, 058D0A48195D05CB00B7D73C /* ASControlNode.m in Headers */, 058D0A49195D05CB00B7D73C /* ASControlNode+Subclasses.h in Headers */, 058D0A4A195D05CB00B7D73C /* ASDisplayNode.h in Headers */, 1950C4491A3BB5C1005C8279 /* ASEqualityHelpers.h in Headers */, + 9C5FA3511B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */, 058D0A4B195D05CB00B7D73C /* ASDisplayNode.mm in Headers */, 430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */, 058D0A4C195D05CB00B7D73C /* ASDisplayNode+Subclasses.h in Headers */, @@ -1041,8 +1123,10 @@ 058D0A69195D05EC00B7D73C /* _ASAsyncTransaction.m in Headers */, 058D0A6A195D05EC00B7D73C /* _ASAsyncTransactionContainer+Private.h in Headers */, 058D0A6B195D05EC00B7D73C /* _ASAsyncTransactionContainer.h in Headers */, + 18C2ED7E1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, 058D0A6C195D05EC00B7D73C /* _ASAsyncTransactionContainer.m in Headers */, 6BDC61F61979037800E50D21 /* AsyncDisplayKit.h in Headers */, + 9CDC18CC1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */, 058D0A6D195D05EC00B7D73C /* _ASAsyncTransactionGroup.h in Headers */, 058D0A6E195D05EC00B7D73C /* _ASAsyncTransactionGroup.m in Headers */, 205F0E1D1B373A2C007741D0 /* ASCollectionViewLayoutController.h in Headers */, @@ -1091,11 +1175,14 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 9C6BB3B31B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, B35062321B010EFD0018CF92 /* ASTextNodeShadower.h in Headers */, + 34EFC7651B701CCC00AD841F /* ASRelativeSize.h in Headers */, B35062431B010EFD0018CF92 /* UIView+ASConvenience.h in Headers */, B31A241E1B0114FD0016AE7A /* AsyncDisplayKit.h in Headers */, B350622D1B010EFD0018CF92 /* ASScrollDirection.h in Headers */, B35061FB1B010EFD0018CF92 /* ASDisplayNode.h in Headers */, + 34EFC7671B701CD900AD841F /* ASLayout.h in Headers */, B35062361B010EFD0018CF92 /* ASTextNodeTypes.h in Headers */, B35062341B010EFD0018CF92 /* ASTextNodeTextKitHelpers.h in Headers */, B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, @@ -1106,17 +1193,23 @@ 430E7C901B4C23F100697A4C /* ASIndexPath.h in Headers */, B35062281B010EFD0018CF92 /* ASRangeHandler.h in Headers */, B35061FD1B010EFD0018CF92 /* ASDisplayNode+Subclasses.h in Headers */, + 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, B35062491B010EFD0018CF92 /* _ASCoreAnimationExtras.h in Headers */, B35061F31B010EFD0018CF92 /* ASCellNode.h in Headers */, + 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, B35062201B010EFD0018CF92 /* ASLayoutController.h in Headers */, B35062571B010F070018CF92 /* ASAssert.h in Headers */, B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */, B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, B350625C1B010F070018CF92 /* ASLog.h in Headers */, B35062551B010EFD0018CF92 /* ASSentinel.h in Headers */, + 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, B350624B1B010EFD0018CF92 /* _ASPendingState.h in Headers */, B35062391B010EFD0018CF92 /* ASThread.h in Headers */, B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */, + 34EFC7791B701D3600AD841F /* ASLayoutSpecUtilities.h in Headers */, + 9C5FA3521B8F6ADF00A62714 /* ASLayoutOptions.h in Headers */, + 34EFC76A1B701CE600AD841F /* ASLayoutSpec.h in Headers */, B35062221B010EFD0018CF92 /* ASMultidimensionalArrayUtils.h in Headers */, B350625B1B010F070018CF92 /* ASEqualityHelpers.h in Headers */, B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */, @@ -1124,6 +1217,7 @@ B35062241B010EFD0018CF92 /* ASMutableAttributedStringBuilder.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, + 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, B350625A1B010F070018CF92 /* ASDisplayNodeExtraIvars.h in Headers */, B350621F1B010EFD0018CF92 /* ASImageProtocols.h in Headers */, B35061DF1B010EDF0018CF92 /* AsyncDisplayKit-iOS.h in Headers */, @@ -1132,14 +1226,22 @@ B350620C1B010EFD0018CF92 /* ASTableViewProtocols.h in Headers */, B35062481B010EFD0018CF92 /* _AS-objc-internal.h in Headers */, B350623F1B010EFD0018CF92 /* _ASAsyncTransactionContainer.h in Headers */, + 9C49C3701B853961000B0DD5 /* ASStackLayoutable.h in Headers */, + 34EFC7731B701D0700AD841F /* ASStaticLayoutSpec.h in Headers */, B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */, B35062581B010F070018CF92 /* ASAvailability.h in Headers */, B35062461B010EFD0018CF92 /* ASBasicImageDownloaderInternal.h in Headers */, B350622B1B010EFD0018CF92 /* ASRangeHandlerRender.h in Headers */, + 34EFC7751B701D2400AD841F /* ASStackPositionedLayout.h in Headers */, + 34EFC7771B701D2D00AD841F /* ASStackUnpositionedLayout.h in Headers */, B350622E1B010EFD0018CF92 /* ASTextNodeCoreTextAdditions.h in Headers */, B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, + 34EFC7691B701CE100AD841F /* ASLayoutable.h in Headers */, + 34EFC75F1B701C8600AD841F /* ASInsetLayoutSpec.h in Headers */, + 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, B350624D1B010EFD0018CF92 /* _ASScopeTimer.h in Headers */, + 34EFC7701B701CFA00AD841F /* ASStackLayoutDefines.h in Headers */, 509E68651B3AEDC5009B9150 /* CGRect+ASConvenience.h in Headers */, B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, B35062211B010EFD0018CF92 /* ASLayoutRangeType.h in Headers */, @@ -1147,18 +1249,24 @@ B35061FE1B010EFD0018CF92 /* ASDisplayNodeExtras.h in Headers */, B35062041B010EFD0018CF92 /* ASMultiplexImageNode.h in Headers */, B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, + 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */, + 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */, B35062301B010EFD0018CF92 /* ASTextNodeRenderer.h in Headers */, 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */, + 9C65A72B1BA8EA4D0084DA91 /* ASLayoutOptionsPrivate.h in Headers */, B350620D1B010EFD0018CF92 /* ASTextNode.h in Headers */, + 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */, B35062151B010EFD0018CF92 /* ASBatchContext.h in Headers */, B350621B1B010EFD0018CF92 /* ASFlowLayoutController.h in Headers */, B35062291B010EFD0018CF92 /* ASRangeHandlerPreload.h in Headers */, B35062001B010EFD0018CF92 /* ASEditableTextNode.h in Headers */, B350623A1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.h in Headers */, B350623E1B010EFD0018CF92 /* _ASAsyncTransactionContainer+Private.h in Headers */, + 9C8221961BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, B35062191B010EFD0018CF92 /* ASDealloc2MainObject.h in Headers */, B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, + 9CDC18CD1B910E12004965E2 /* ASLayoutablePrivate.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1362,6 +1470,8 @@ ACF6ED2E1B17843500DA7C62 /* ASRatioLayoutSpec.mm in Sources */, 058D0A18195D050800B7D73C /* _ASDisplayLayer.mm in Sources */, ACF6ED321B17843500DA7C62 /* ASStaticLayoutSpec.mm in Sources */, + 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, + 9C5FA35F1B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */, ACF6ED2C1B17843500DA7C62 /* ASOverlayLayoutSpec.mm in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 205F0E221B376416007741D0 /* CGRect+ASConvenience.m in Sources */, @@ -1374,8 +1484,9 @@ ACF6ED1B1B17843500DA7C62 /* ASBackgroundLayoutSpec.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, 205F0E121B371BD7007741D0 /* ASScrollDirection.m in Sources */, - AC47D9461B3BB41900AAEE9D /* ASStaticLayoutSpecDimension.mm in Sources */, + AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */, ACF6ED271B17843500DA7C62 /* ASLayoutSpec.mm in Sources */, + 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, ACF6ED211B17843500DA7C62 /* ASDimension.mm in Sources */, 464052261A3F83C40061C0BA /* ASMultidimensionalArrayUtils.mm in Sources */, 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, @@ -1391,6 +1502,7 @@ 464052231A3F83C40061C0BA /* ASFlowLayoutController.mm in Sources */, 058D0A28195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm in Sources */, 0587F9BE1A7309ED00AFF0BA /* ASEditableTextNode.mm in Sources */, + 18C2ED801B9B7DE800F627B3 /* ASCollectionNode.m in Sources */, 058D0A21195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.m in Sources */, ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */, 058D0A25195D050800B7D73C /* UIView+ASConvenience.m in Sources */, @@ -1458,18 +1570,27 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.m in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, B35062311B010EFD0018CF92 /* ASTextNodeRenderer.mm in Sources */, B35062051B010EFD0018CF92 /* ASMultiplexImageNode.mm in Sources */, B35061FC1B010EFD0018CF92 /* ASDisplayNode.mm in Sources */, B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */, + 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, B35062471B010EFD0018CF92 /* ASBatchFetching.m in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, + 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, + 9C5FA3601B90C9A500A62714 /* ASLayoutOptionsPrivate.mm in Sources */, + 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */, + 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, + 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */, + 34EFC7661B701CD200AD841F /* ASRelativeSize.mm in Sources */, B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, + 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.m in Sources */, B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, @@ -1477,8 +1598,10 @@ B350621C1B010EFD0018CF92 /* ASFlowLayoutController.mm in Sources */, B35062231B010EFD0018CF92 /* ASMultidimensionalArrayUtils.mm in Sources */, 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.mm in Sources */, + 9C5FA3541B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, B350621E1B010EFD0018CF92 /* ASHighlightOverlayLayer.mm in Sources */, B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, + 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.m in Sources */, B35061F91B010EFD0018CF92 /* ASControlNode.m in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.m in Sources */, 509E68661B3AEDD7009B9150 /* CGRect+ASConvenience.m in Sources */, @@ -1487,18 +1610,25 @@ B35062071B010EFD0018CF92 /* ASNetworkImageNode.mm in Sources */, B35062011B010EFD0018CF92 /* ASEditableTextNode.mm in Sources */, B35062441B010EFD0018CF92 /* UIView+ASConvenience.m in Sources */, + 34EFC7761B701D2A00AD841F /* ASStackPositionedLayout.mm in Sources */, B350622F1B010EFD0018CF92 /* ASTextNodeCoreTextAdditions.m in Sources */, + 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */, B35062031B010EFD0018CF92 /* ASImageNode.mm in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.m in Sources */, B35062251B010EFD0018CF92 /* ASMutableAttributedStringBuilder.m in Sources */, + 9C8221981BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, 430E7C921B4C23F100697A4C /* ASIndexPath.m in Sources */, + 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */, B35062381B010EFD0018CF92 /* ASTextNodeWordKerner.m in Sources */, B35062101B010EFD0018CF92 /* _ASDisplayLayer.mm in Sources */, B35062351B010EFD0018CF92 /* ASTextNodeTextKitHelpers.mm in Sources */, B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */, B35061FF1B010EFD0018CF92 /* ASDisplayNodeExtras.mm in Sources */, + 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, B35062121B010EFD0018CF92 /* _ASDisplayView.mm in Sources */, + 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, B350624C1B010EFD0018CF92 /* _ASPendingState.m in Sources */, + 34EFC75E1B701BF000AD841F /* ASInternalHelpers.mm in Sources */, B35062541B010EFD0018CF92 /* ASImageNode+CGExtras.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B350622C1B010EFD0018CF92 /* ASRangeHandlerRender.mm in Sources */, @@ -1747,7 +1877,7 @@ "DEBUG=1", "$(inherited)", ); - INFOPLIST_FILE = AsyncDisplayKit/Info.plist; + INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit-iOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -1774,7 +1904,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = AsyncDisplayKit/Info.plist; + INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit-iOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index 39aadab6bb..80bc34598c 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -31,13 +31,13 @@ return self; } -- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; } -- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; @@ -106,12 +106,12 @@ static const CGFloat kFontSize = 18.0f; return self; } -- (id)layoutSpecThatFits:(ASSizeRange)constrainedSize +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { static const CGFloat kHorizontalPadding = 15.0f; static const CGFloat kVerticalPadding = 11.0f; UIEdgeInsets insets = UIEdgeInsetsMake(kVerticalPadding, kHorizontalPadding, kVerticalPadding, kHorizontalPadding); - return [ASInsetLayoutSpec newWithInsets:insets child:_textNode]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:_textNode]; } - (void)setText:(NSString *)text diff --git a/AsyncDisplayKit/ASCollectionNode.h b/AsyncDisplayKit/ASCollectionNode.h new file mode 100644 index 0000000000..c0489b2297 --- /dev/null +++ b/AsyncDisplayKit/ASCollectionNode.h @@ -0,0 +1,21 @@ +// +// ASCollectionNode.h +// AsyncDisplayKit +// +// Created by Scott Goodson on 9/5/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +/** + * ASCollectionNode is a node based class that wraps an ASCollectionView. It can be used + * as a subnode of another node, and provide room for many (great) features and improvements later on. + */ +@interface ASCollectionNode : ASDisplayNode + +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) ASCollectionView *view; + +@end diff --git a/AsyncDisplayKit/ASCollectionNode.m b/AsyncDisplayKit/ASCollectionNode.m new file mode 100644 index 0000000000..2a131bfecd --- /dev/null +++ b/AsyncDisplayKit/ASCollectionNode.m @@ -0,0 +1,33 @@ +// +// ASCollectionNode.m +// AsyncDisplayKit +// +// Created by Scott Goodson on 9/5/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "ASCollectionNode.h" + +@implementation ASCollectionNode + +- (instancetype)init +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + self = [self initWithCollectionViewLayout:nil]; // Will throw an exception for lacking a UICV Layout. + return nil; +} + +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout +{ + if (self = [super initWithViewBlock:^UIView *{ return [[ASCollectionView alloc] initWithCollectionViewLayout:layout]; }]) { + return self; + } + return nil; +} + +- (ASCollectionView *)view +{ + return (ASCollectionView *)[super view]; +} + +@end diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index ec6fd3242e..4756835551 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -27,15 +27,17 @@ */ @interface ASCollectionView : UICollectionView +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout; + @property (nonatomic, weak) id asyncDataSource; @property (nonatomic, weak) id asyncDelegate; // must not be nil /** - * Tuning parameters for a range. + * Tuning parameters for a range type. * - * @param range The range to get the tuning parameters for. + * @param rangeType The range type to get the tuning parameters for. * - * @returns A tuning parameter value for the given range. + * @returns A tuning parameter value for the given range type. * * Defaults to the render range having one sceenful both leading and trailing and the preload range having two * screenfuls in both directions. @@ -43,16 +45,24 @@ - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType; /** - * Set the tuning parameters for a range. + * Set the tuning parameters for a range type. * - * @param tuningParameters The tuning parameters to store for a range. - * @param range The range to set the tuning parameters for. + * @param tuningParameters The tuning parameters to store for a range type. + * @param rangeType The range type to set the tuning parameters for. */ - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; /** * Initializer. * + * @param frame The frame rectangle for the collection view, measured in points. The origin of the frame is relative to the superview + * in which you plan to add it. This frame is passed to the superclass during initialization. + * + * @param layout The layout object to use for organizing items. The collection view stores a strong reference to the specified object. + * Must not be nil. + * + * @param asyncDataFetchingEnabled Enable the data fetching in async mode. + * * @discussion If asyncDataFetching is enabled, the `AScollectionView` will fetch data through `collectionView:numberOfRowsInSection:` and * `collectionView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically * from calling thread. @@ -70,6 +80,29 @@ */ @property (nonatomic, assign) CGFloat leadingScreensForBatching; +/** + * Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before the update block completes. + * + * @param animated NO to disable animations for this batch + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion; + +/** + * Perform a batch of updates asynchronously. This method must be called from the main thread. + * The asyncDataSource must be updated to reflect the changes before update block completes. + * + * @param updates The block that performs the relevant insert, delete, reload, or move operations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion; + /** * Reload everything from scratch, destroying the working range and all cached nodes. * @@ -87,25 +120,87 @@ - (void)reloadData; /** - * Section updating. + * Inserts one or more sections. * - * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection - * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + * @param sections An index set that specifies the sections to insert. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. */ - (void)insertSections:(NSIndexSet *)sections; + +/** + * Deletes one or more sections. + * + * @param sections An index set that specifies the sections to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)deleteSections:(NSIndexSet *)sections; + +/** + * Reloads the specified sections. + * + * @param sections An index set that specifies the sections to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)reloadSections:(NSIndexSet *)sections; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; /** - * Items updating. + * Inserts items at the locations identified by an array of index paths. * - * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection - * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + * @param indexPaths An array of NSIndexPath objects, each representing an item index and section index that together identify an item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. */ - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Deletes the items specified by an array of index paths. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to delete. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Reloads the specified items. + * + * @param indexPaths An array of NSIndexPath objects identifying the items to reload. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; + +/** + * Moves the item at a specified location to a destination location. + * + * @param indexPath The index path identifying the item to move. + * + * @param newIndexPath The index path that is the destination of the move for the item. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; /** @@ -156,7 +251,7 @@ /** * Similar to -collectionView:cellForItemAtIndexPath:. * - * @param collection The sender. + * @param collectionView The sender. * * @param indexPath The index path of the requested node. * @@ -168,6 +263,17 @@ @optional +/** + * Provides the constrained size range for measuring the node at the index path. + * + * @param collectionView The sender. + * + * @param indexPath The index path of the node. + * + * @returns A constrained size range for layout the node at this index path. + */ +- (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; + /** * Indicator to lock the data source for data fetching in async mode. * We should not update the data source until the data source has been unlocked. Otherwise, it will incur data inconsistence or exception @@ -227,6 +333,19 @@ */ - (BOOL)shouldBatchFetchForCollectionView:(ASCollectionView *)collectionView; +/** + * Passthrough support to UICollectionViewDelegateFlowLayout sectionInset behavior. + * + * @param collectionView The sender. + * @param collectionViewLayout The layout object requesting the information. + * #param section The index number of the section whose insets are needed. + * + * @discussion The same rules apply as the UICollectionView implementation, but this can also be used without a UICollectionViewFlowLayout. + * https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionViewDelegateFlowLayout_protocol/index.html#//apple_ref/occ/intfm/UICollectionViewDelegateFlowLayout/collectionView:layout:insetForSectionAtIndex: + * + */ +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; + @end @interface ASCollectionView (Deprecated) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 36a995f90b..cf54d6fbde 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -15,6 +15,7 @@ #import "ASDisplayNodeInternal.h" #import "ASBatchFetching.h" #import "UICollectionViewLayout+ASConvenience.h" +#import "ASInternalHelpers.h" const static NSUInteger kASCollectionViewAnimationNone = UITableViewRowAnimationNone; @@ -33,6 +34,9 @@ static BOOL _isInterceptedSelector(SEL sel) // handled by ASCollectionView node<->cell machinery sel == @selector(collectionView:cellForItemAtIndexPath:) || sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + + // TODO: Supplementary views are currently not supported. An assertion is triggered if the _asyncDataSource implements this method. + // sel == @selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:) || // handled by ASRangeController sel == @selector(numberOfSectionsInCollectionView:) || @@ -115,8 +119,14 @@ static BOOL _isInterceptedSelector(SEL sel) NSMutableArray *_batchUpdateBlocks; BOOL _asyncDataFetchingEnabled; + BOOL _asyncDelegateImplementsInsetSection; + BOOL _collectionViewLayoutImplementsInsetSection; + BOOL _asyncDataSourceImplementsConstrainedSizeForNode; ASBatchContext *_batchContext; + + CGSize _maxSizeForNodesConstrainedSize; + BOOL _ignoreMaxSizeChange; } @property (atomic, assign) BOOL asyncDataSourceLocked; @@ -128,6 +138,11 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark - #pragma mark Lifecycle. +- (instancetype)initWithCollectionViewLayout:(UICollectionViewLayout *)layout +{ + return [self initWithFrame:CGRectZero collectionViewLayout:layout asyncDataFetching:NO]; +} + - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout { return [self initWithFrame:frame collectionViewLayout:layout asyncDataFetching:NO]; @@ -162,6 +177,15 @@ static BOOL _isInterceptedSelector(SEL sel) _performingBatchUpdates = NO; _batchUpdateBlocks = [NSMutableArray array]; + _collectionViewLayoutImplementsInsetSection = [layout respondsToSelector:@selector(sectionInset)]; + + _maxSizeForNodesConstrainedSize = self.bounds.size; + // 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. + _ignoreMaxSizeChange = CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, CGSizeZero); + + self.backgroundColor = [UIColor whiteColor]; + [self registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"_ASCollectionViewCell"]; return self; @@ -214,10 +238,16 @@ static BOOL _isInterceptedSelector(SEL sel) super.dataSource = nil; _asyncDataSource = nil; _proxyDataSource = nil; + _asyncDataSourceImplementsConstrainedSizeForNode = NO; } else { _asyncDataSource = asyncDataSource; + // TODO: Support supplementary views with ASCollectionView. + if ([_asyncDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]) { + ASDisplayNodeAssert(NO, @"ASCollectionView is planned to support supplementary views by September 2015. You can work around this issue by using standard items."); + } _proxyDataSource = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDataSource interceptor:self]; super.dataSource = (id)_proxyDataSource; + _asyncDataSourceImplementsConstrainedSizeForNode = ([_asyncDataSource respondsToSelector:@selector(collectionView:constrainedSizeForNodeAtIndexPath:)] ? 1 : 0); } } @@ -234,10 +264,12 @@ static BOOL _isInterceptedSelector(SEL sel) super.delegate = nil; _asyncDelegate = nil; _proxyDelegate = nil; + _asyncDelegateImplementsInsetSection = NO; } else { _asyncDelegate = asyncDelegate; _proxyDelegate = [[_ASCollectionViewProxy alloc] initWithTarget:_asyncDelegate interceptor:self]; super.delegate = (id)_proxyDelegate; + _asyncDelegateImplementsInsetSection = ([_asyncDelegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)] ? 1 : 0); } } @@ -281,50 +313,65 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark Assertions. -- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion +- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion { + ASDisplayNodeAssertMainThread(); + [_dataController beginUpdates]; updates(); - [_dataController endUpdatesWithCompletion:completion]; + [_dataController endUpdatesAnimated:animated completion:completion]; +} + +- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion +{ + [self performBatchAnimated:YES updates:updates completion:completion]; } - (void)insertSections:(NSIndexSet *)sections { + ASDisplayNodeAssertMainThread(); [_dataController insertSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)deleteSections:(NSIndexSet *)sections { + ASDisplayNodeAssertMainThread(); [_dataController deleteSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)reloadSections:(NSIndexSet *)sections { + ASDisplayNodeAssertMainThread(); [_dataController reloadSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { + ASDisplayNodeAssertMainThread(); [_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssertMainThread(); [_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssertMainThread(); [_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { + ASDisplayNodeAssertMainThread(); [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { + ASDisplayNodeAssertMainThread(); [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone]; } @@ -436,6 +483,26 @@ static BOOL _isInterceptedSelector(SEL sel) } } +- (void)layoutSubviews +{ + if (! CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, self.bounds.size)) { + _maxSizeForNodesConstrainedSize = self.bounds.size; + + // First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time + // and should be avoided, assuming that the initial data loading automatically runs shortly afterward. + if (_ignoreMaxSizeChange) { + _ignoreMaxSizeChange = NO; + } else { + [self performBatchAnimated:NO updates:^{ + [_dataController relayoutAllRows]; + } completion:nil]; + } + } + + // To ensure _maxSizeForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last + [super layoutSubviews]; +} + #pragma mark - #pragma mark Batch Fetching @@ -486,17 +553,45 @@ static BOOL _isInterceptedSelector(SEL sel) return node; } -- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { - CGSize restrainedSize = self.bounds.size; - - if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { - restrainedSize.width = FLT_MAX; + ASSizeRange constrainedSize; + if (_asyncDataSourceImplementsConstrainedSizeForNode) { + constrainedSize = [_asyncDataSource collectionView:self constrainedSizeForNodeAtIndexPath:indexPath]; } else { - restrainedSize.height = FLT_MAX; + CGSize maxSize = _maxSizeForNodesConstrainedSize; + if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { + maxSize.width = FLT_MAX; + } else { + maxSize.height = FLT_MAX; + } + constrainedSize = ASSizeRangeMake(CGSizeZero, maxSize); } - return restrainedSize; + UIEdgeInsets sectionInset = UIEdgeInsetsZero; + if (_collectionViewLayoutImplementsInsetSection) { + sectionInset = [(UICollectionViewFlowLayout *)self.collectionViewLayout sectionInset]; + } + + if (_asyncDelegateImplementsInsetSection) { + sectionInset = [_asyncDelegate collectionView:self layout:self.collectionViewLayout insetForSectionAtIndex:indexPath.section]; + } + + if (ASScrollDirectionContainsHorizontalDirection([self scrollableDirections])) { + constrainedSize.min.width = MAX(0, constrainedSize.min.width - sectionInset.left - sectionInset.right); + //ignore insets for FLT_MAX so FLT_MAX can be compared against + if (constrainedSize.max.width - FLT_EPSILON < FLT_MAX) { + constrainedSize.max.width = MAX(0, constrainedSize.max.width - sectionInset.left - sectionInset.right); + } + } else { + constrainedSize.min.height = MAX(0, constrainedSize.min.height - sectionInset.top - sectionInset.bottom); + //ignore insets for FLT_MAX so FLT_MAX can be compared against + if (constrainedSize.max.height - FLT_EPSILON < FLT_MAX) { + constrainedSize.max.height = MAX(0, constrainedSize.max.height - sectionInset.top - sectionInset.bottom); + } + } + + return constrainedSize; } - (NSUInteger)dataController:(ASDataController *)dataController rowsInSection:(NSUInteger)section @@ -540,14 +635,31 @@ static BOOL _isInterceptedSelector(SEL sel) _performingBatchUpdates = YES; } -- (void)rangeControllerEndUpdates:(ASRangeController *)rangeController completion:(void (^)(BOOL))completion { +- (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource) { + if (completion) { + completion(NO); + } + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + BOOL animationsEnabled = NO; + + if (!animated) { + animationsEnabled = [UIView areAnimationsEnabled]; + [UIView setAnimationsEnabled:NO]; + } + [super performBatchUpdates:^{ [_batchUpdateBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { block(); }]; } completion:^(BOOL finished) { + if (!animated) { + [UIView setAnimationsEnabled:animationsEnabled]; + } if (completion) { completion(finished); } @@ -574,9 +686,14 @@ static BOOL _isInterceptedSelector(SEL sel) return [_dataController nodesAtIndexPaths:indexPaths]; } -- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + if (_performingBatchUpdates) { [_batchUpdateBlocks addObject:^{ [super insertItemsAtIndexPaths:indexPaths]; @@ -588,10 +705,14 @@ static BOOL _isInterceptedSelector(SEL sel) } } -- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + if (_performingBatchUpdates) { [_batchUpdateBlocks addObject:^{ [super deleteItemsAtIndexPaths:indexPaths]; @@ -607,6 +728,10 @@ static BOOL _isInterceptedSelector(SEL sel) { ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + if (_performingBatchUpdates) { [_batchUpdateBlocks addObject:^{ [super insertSections:indexSet]; @@ -622,6 +747,10 @@ static BOOL _isInterceptedSelector(SEL sel) { ASDisplayNodeAssertMainThread(); + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + if (_performingBatchUpdates) { [_batchUpdateBlocks addObject:^{ [super deleteSections:indexSet]; diff --git a/AsyncDisplayKit/ASControlNode.h b/AsyncDisplayKit/ASControlNode.h index b94bb77227..d08a09c69a 100644 --- a/AsyncDisplayKit/ASControlNode.h +++ b/AsyncDisplayKit/ASControlNode.h @@ -15,14 +15,21 @@ */ typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent) { + /** A touch-down event in the control node. */ ASControlNodeEventTouchDown = 1 << 0, + /** A repeated touch-down event in the control node; for this event the value of the UITouch tapCount method is greater than one. */ ASControlNodeEventTouchDownRepeat = 1 << 1, + /** An event where a finger is dragged inside the bounds of the control node. */ ASControlNodeEventTouchDragInside = 1 << 2, + /** An event where a finger is dragged just outside the bounds of the control. */ ASControlNodeEventTouchDragOutside = 1 << 3, + /** A touch-up event in the control node where the finger is inside the bounds of the node. */ ASControlNodeEventTouchUpInside = 1 << 4, + /** A touch-up event in the control node where the finger is outside the bounds of the node. */ ASControlNodeEventTouchUpOutside = 1 << 5, + /** A system event canceling the current touches for the control node. */ ASControlNodeEventTouchCancel = 1 << 6, - + /** All events, including system events. */ ASControlNodeEventAllEvents = 0xFFFFFFFF }; diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index fec0527f27..44b81ac1b1 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -13,8 +13,7 @@ #import #import -#import -#import +@class ASLayoutSpec; /** * The subclass header _ASDisplayNode+Subclasses_ defines the following methods that either must or can be overriden by @@ -36,7 +35,7 @@ * variables. */ -@interface ASDisplayNode (Subclassing) +@interface ASDisplayNode (Subclassing) /** @name View Configuration */ @@ -120,23 +119,6 @@ /** @name Layout calculation */ -/** - * @abstract Asks the node to measure a layout based on given size range. - * - * @param constrainedSize The minimum and maximum sizes the receiver should fit in. - * - * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). - * - * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the - * constraint and the result. - * - * @warning Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may - * be expensive if result is not cached. - * - * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] - */ -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize; - /** * @abstract Calculate a layout based on given size range. * @@ -180,7 +162,7 @@ * * @note This method should not be called directly outside of ASDisplayNode; use -measure: or -calculatedLayout instead. */ -- (id)layoutSpecThatFits:(ASSizeRange)constrainedSize; +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize; /** * @abstract Invalidate previously measured and cached layout. @@ -267,6 +249,8 @@ * @abstract Indicates that the receiver is about to display its subnodes. This method is not called if there are no * subnodes present. * + * @param subnode The subnode of which display is about to begin. + * * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is * about to begin. */ @@ -276,6 +260,8 @@ * @abstract Indicates that the receiver is finished displaying its subnodes. This method is not called if there are * no subnodes present. * + * @param subnode The subnode of which display is about to completed. + * * @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) has * completed. */ diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index bd0c5727ab..11a138ea4f 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -13,9 +13,25 @@ #import #import +#import + +@class ASDisplayNode; + +/** + * UIView creation block. Used to create the backing view of a new display node. + */ typedef UIView *(^ASDisplayNodeViewBlock)(); + +/** + * CALayer creation block. Used to create the backing layer of a new display node. + */ typedef CALayer *(^ASDisplayNodeLayerBlock)(); +/** + * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. + */ +typedef void (^ASDisplayNodeDidLoadBlock)(ASDisplayNode *node); + /** * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view * hierarchy off the main thread, and could do rendering off the main thread as well. @@ -32,7 +48,7 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); * */ -@interface ASDisplayNode : ASDealloc2MainObject +@interface ASDisplayNode : ASDealloc2MainObject /** @name Initializing a node object */ @@ -50,18 +66,44 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); /** * @abstract Alternative initializer with a block to create the backing view. * + * @param viewBlock The block that will be used to create the backing view. + * * @return An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main * queue. The view will render synchronously and -layout and touch handling methods on the node will not be called. */ - (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock; +/** + * @abstract Alternative initializer with a block to create the backing view. + * + * @param viewBlock The block that will be used to create the backing view. + * @param didLoadBlock The block that will be called after the view created by the viewBlock is loaded + * + * @return An ASDisplayNode instance that loads its view with the given block that is guaranteed to run on the main + * queue. The view will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock; + /** * @abstract Alternative initializer with a block to create the backing layer. * + * @param layerBlock The block that will be used to create the backing layer. + * * @return An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main * queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called. */ -- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock; +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock; + +/** + * @abstract Alternative initializer with a block to create the backing layer. + * + * @param layerBlock The block that will be used to create the backing layer. + * @param didLoadBlock The block that will be called after the layer created by the layerBlock is loaded + * + * @return An ASDisplayNode instance that loads its layer with the given block that is guaranteed to run on the main + * queue. The layer will render synchronously and -layout and touch handling methods on the node will not be called. + */ +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock; /** @name Properties */ @@ -132,11 +174,28 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); * -measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may * be expensive if result is not cached. * - * @see [ASDisplayNode(Subclassing) measureWithSizeRange:] + * @see measureWithSizeRange: * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] */ - (CGSize)measure:(CGSize)constrainedSize; +/** + * @abstract Asks the node to measure a layout based on given size range. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used). + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @warning Subclasses must not override this; it caches results from -calculateLayoutThatFits:. Calling this method may + * be expensive if result is not cached. + * + * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + */ +- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize; + /** * @abstract Return the calculated size. * @@ -156,6 +215,16 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); */ @property (nonatomic, readonly, assign) ASSizeRange constrainedSizeForCalculatedLayout; +/** + * @abstract Provides a default intrinsic content size for calculateSizeThatFits:. This is useful when laying out + * a node that either has no intrinsic content size or should be laid out at a different size than its intrinsic content + * size. For example, this property could be set on an ASImageNode to display at a size different from the underlying + * image size. + * + * @return The preferred frame size of this node + */ +@property (atomic, assign, readwrite) CGSize preferredFrameSize; + /** @name Managing the nodes hierarchy */ @@ -332,7 +401,7 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); * This method is used to notify the node that it should purge any content that is both expensive to fetch and to * retain in memory. * - * @see clearFetchedData and fetchData + * @see [ASDisplayNode(Subclassing) clearFetchedData] and [ASDisplayNode(Subclassing) fetchData] */ - (void)recursivelyClearFetchedData; @@ -341,7 +410,7 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); * * @discussion Fetches content from remote sources for the current node and all subnodes. * - * @see fetchData and clearFetchedData + * @see [ASDisplayNode(Subclassing) fetchData] and [ASDisplayNode(Subclassing) clearFetchedData] */ - (void)recursivelyFetchData; @@ -476,8 +545,22 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); */ @interface ASDisplayNode (UIViewBridge) -- (void)setNeedsDisplay; // Marks the view as needing display. Convenience for use whether view is created or not, or from a background thread. -- (void)setNeedsLayout; // Marks the view as needing layout. Convenience for use whether view is created or not, or from a background thread. +/** + * Marks the view as needing display. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + */ +- (void)setNeedsDisplay; + +/** + * Marks the node as needing layout. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread. + * + * If this node was measured, calling this method triggers an internal relayout: the calculated layout is invalidated, + * and the supernode is notified or (if this node is the root one) a full measurement pass is executed using the old constrained size. + * + * Note: If the relayout causes a change in size of the root node that is attached to a container view + * (table or collection view, for example), the container view must be notified to relayout. + * For ASTableView and ASCollectionView, an empty batch editing transaction must be triggered to animate to new row / item sizes. + */ +- (void)setNeedsLayout; @property (atomic, retain) id contents; // default=nil @property (atomic, assign) BOOL clipsToBounds; // default==NO @@ -556,18 +639,22 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); @interface ASDisplayNode (ASDisplayNodeAsyncTransactionContainer) @end - +/** UIVIew(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to an UIView. */ @interface UIView (AsyncDisplayKit) /** * Convenience method, equivalent to [view addSubview:node.view] or [view.layer addSublayer:node.layer] if layer-backed. + * + * @param node The node to be added. */ - (void)addSubnode:(ASDisplayNode *)node; @end - +/** CALayer(AsyncDisplayKit) defines convenience method for adding sub-ASDisplayNode to a CALayer. */ @interface CALayer (AsyncDisplayKit) /** * Convenience method, equivalent to [layer addSublayer:node.layer]. + * + * @param node The node to be added. */ - (void)addSubnode:(ASDisplayNode *)node; @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index bf81cdc960..323ade43e5 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -9,6 +9,7 @@ #import "ASDisplayNode.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeInternal.h" +#import "ASLayoutOptionsPrivate.h" #import @@ -19,6 +20,8 @@ #import "ASDisplayNodeExtras.h" #import "ASInternalHelpers.h" +#import "ASLayout.h" +#import "ASLayoutSpec.h" @interface ASDisplayNode () @@ -28,6 +31,8 @@ * */ +- (void)_staticInitialize; + @end // Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) @@ -39,12 +44,10 @@ @implementation ASDisplayNode -@synthesize spacingBefore = _spacingBefore; -@synthesize spacingAfter = _spacingAfter; -@synthesize flexGrow = _flexGrow; -@synthesize flexShrink = _flexShrink; -@synthesize flexBasis = _flexBasis; -@synthesize alignSelf = _alignSelf; +// these dynamic properties all defined in ASLayoutOptionsPrivate.m +@dynamic spacingAfter, spacingBefore, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange, layoutPosition, layoutOptions; +@synthesize preferredFrameSize = _preferredFrameSize; +@synthesize isFinalLayoutable = _isFinalLayoutable; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) { @@ -62,25 +65,124 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) } } -+ (void)initialize +void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)()) { - if (self == [ASDisplayNode class]) { + ASDisplayNodeCAssertNotNil(block, @"block is required"); + if (!block) { return; } - // Subclasses should never override these - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method", NSStringFromClass(self)); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method", NSStringFromClass(self)); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measure:)), @"Subclass %@ must not override measure method", NSStringFromClass(self)); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange method", NSStringFromClass(self)); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method", NSStringFromClass(self)); - ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearFetchedData)), @"Subclass %@ must not override recursivelyClearFetchedData method", NSStringFromClass(self)); + { + // Hold the lock to avoid a race where the node gets loaded while the block is in-flight. + ASDN::MutexLocker l(node->_propertyLock); + if (node.nodeLoaded) { + ASDisplayNodePerformBlockOnMainThread(^{ + block(); + }); + } else { + block(); + } + } +} - // At most one of the three layout methods is overridden - ASDisplayNodeAssert((ASDisplayNodeSubclassOverridesSelector(self, @selector(calculateSizeThatFits:)) ? 1 : 0) - + (ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutSpecThatFits:)) ? 1 : 0) - + (ASDisplayNodeSubclassOverridesSelector(self, @selector(calculateLayoutThatFits:)) ? 1 : 0) <= 1, - @"Subclass %@ must override at most one of the three layout methods: calculateLayoutThatFits, layoutSpecThatFits or calculateSizeThatFits", NSStringFromClass(self)); +/** + * Returns ASDisplayNodeFlags for the givern class/instance. instance MAY BE NIL. + * + * @param c the class, required + * @param instance the instance, which may be nil. (If so, the class is inspected instead) + * @remarks The instance value is used only if we suspect the class may be dynamic (because it overloads + * +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", calling this + * method on each -init and passing the instance value. While this may seem like an unlikely scenario, + * it turns our our own internal tests use a dynamic class, so it's worth capturing this edge case. + * + * @return ASDisplayNode flags. + */ +static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *instance) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + struct ASDisplayNodeFlags flags = {0}; + + flags.isInHierarchy = NO; + flags.displaysAsynchronously = YES; + flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); + flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); + if (instance) { + flags.implementsDrawParameters = ([instance respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } else { + flags.implementsDrawParameters = ([c instancesRespondToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } + return flags; +} + +/** + * Returns ASDisplayNodeMethodOverrides for the given class + * + * @param c the class, requireed. + * + * @return ASDisplayNodeMethodOverrides. + */ +static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesBegan:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesBegan; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesMoved:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesMoved; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesCancelled:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesCancelled; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesEnded:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesEnded; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; + } + + + return overrides; +} + ++ (void)initialize +{ + if (self != [ASDisplayNode class]) { + + // Subclasses should never override these + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measure:)), @"Subclass %@ must not override measure method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measureWithSizeRange:)), @"Subclass %@ must not override measureWithSizeRange method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearFetchedData)), @"Subclass %@ must not override recursivelyClearFetchedData method", NSStringFromClass(self)); + + // At most one of the three layout methods is overridden + ASDisplayNodeAssert((ASDisplayNodeSubclassOverridesSelector(self, @selector(calculateSizeThatFits:)) ? 1 : 0) + + (ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutSpecThatFits:)) ? 1 : 0) + + (ASDisplayNodeSubclassOverridesSelector(self, @selector(calculateLayoutThatFits:)) ? 1 : 0) <= 1, + @"Subclass %@ must override at most one of the three layout methods: calculateLayoutThatFits, layoutSpecThatFits or calculateSizeThatFits", NSStringFromClass(self)); + } + + // Below we are pre-calculating values per-class and dynamically adding a method (_staticInitialize) to populate these values + // when each instance is constructed. These values don't change for each class, so there is significant performance benefit + // in doing it here. +initialize is guaranteed to be called before any instance method so it is safe to add this method here. + // Note that we take care to detect if the class overrides +respondsToSelector: or -respondsToSelector and take the slow path + // (recalculating for each instance) to make sure we are always correct. + + BOOL classOverridesRespondsToSelector = ASSubclassOverridesClassSelector([NSObject class], self, @selector(respondsToSelector:)); + BOOL instancesOverrideRespondsToSelector = ASSubclassOverridesSelector([NSObject class], self, @selector(respondsToSelector:)); + struct ASDisplayNodeFlags flags = GetASDisplayNodeFlags(self, nil); + ASDisplayNodeMethodOverrides methodOverrides = GetASDisplayNodeMethodOverrides(self); + + IMP staticInitialize = imp_implementationWithBlock(^(ASDisplayNode *node) { + node->_flags = (classOverridesRespondsToSelector || instancesOverrideRespondsToSelector) ? GetASDisplayNodeFlags(node.class, node) : flags; + node->_methodOverrides = (classOverridesRespondsToSelector) ? GetASDisplayNodeMethodOverrides(node.class) : methodOverrides; + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); } + (BOOL)layerBackedNodesEnabled @@ -100,48 +202,26 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) #pragma mark - Lifecycle +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + - (void)_initializeInstance { + [self _staticInitialize]; _contentsScaleForDisplay = ASScreenScale(); - _displaySentinel = [[ASSentinel alloc] init]; - - _flags.isInHierarchy = NO; - _flags.displaysAsynchronously = YES; - - // As an optimization, it may be worth a caching system that performs these checks once per class in +initialize (see above). - _flags.implementsDrawRect = ([[self class] respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); - _flags.implementsImageDisplay = ([[self class] respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); - _flags.implementsDrawParameters = ([self respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); - - ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; - if (ASDisplayNodeSubclassOverridesSelector([self class], @selector(touchesBegan:withEvent:))) { - overrides |= ASDisplayNodeMethodOverrideTouchesBegan; - } - if (ASDisplayNodeSubclassOverridesSelector([self class], @selector(touchesMoved:withEvent:))) { - overrides |= ASDisplayNodeMethodOverrideTouchesMoved; - } - if (ASDisplayNodeSubclassOverridesSelector([self class], @selector(touchesCancelled:withEvent:))) { - overrides |= ASDisplayNodeMethodOverrideTouchesCancelled; - } - if (ASDisplayNodeSubclassOverridesSelector([self class], @selector(touchesEnded:withEvent:))) { - overrides |= ASDisplayNodeMethodOverrideTouchesEnded; - } - if (ASDisplayNodeSubclassOverridesSelector([self class], @selector(calculateSizeThatFits:))) { - overrides |= ASDisplayNodeMethodOverrideCalculateSizeThatFits; - } - _methodOverrides = overrides; - - _flexBasis = ASRelativeDimensionUnconstrained; + _preferredFrameSize = CGSizeZero; } - (id)init { if (!(self = [super init])) return nil; - + [self _initializeInstance]; - + return self; } @@ -151,7 +231,7 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) return nil; ASDisplayNodeAssert([viewClass isSubclassOfClass:[UIView class]], @"should initialize with a subclass of UIView"); - + [self _initializeInstance]; _viewClass = viewClass; _flags.synchronous = ![viewClass isSubclassOfClass:[_ASDisplayView class]]; @@ -163,7 +243,7 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) { if (!(self = [super init])) return nil; - + ASDisplayNodeAssert([layerClass isSubclassOfClass:[CALayer class]], @"should initialize with a subclass of CALayer"); [self _initializeInstance]; @@ -176,33 +256,47 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) - (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock { - if (!(self = [super init])) - return nil; - - ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); - - [self _initializeInstance]; - _viewBlock = viewBlock; - _flags.synchronous = YES; - - return self; + return [self initWithViewBlock:viewBlock didLoadBlock:nil]; } -- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +- (id)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { if (!(self = [super init])) return nil; - - ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); - + + ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); + [self _initializeInstance]; - _layerBlock = layerBlock; + _viewBlock = viewBlock; + _nodeLoadedBlock = didLoadBlock; _flags.synchronous = YES; - _flags.layerBacked = YES; - + return self; } + +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + return [self initWithLayerBlock:layerBlock didLoadBlock:nil]; +} + +- (id)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [super init])) + return nil; + + ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); + + [self _initializeInstance]; + _layerBlock = layerBlock; + _nodeLoadedBlock = didLoadBlock; + _flags.synchronous = YES; + _flags.layerBacked = YES; + + return self; +} + + - (void)dealloc { ASDisplayNodeAssertMainThread(); @@ -344,7 +438,7 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) } { TIME_SCOPED(_debugTimeForDidLoad); - [self didLoad]; + [self __didLoad]; } if (self.placeholderEnabled) { @@ -482,7 +576,7 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) } /** - * Core implementation of -displaysAsynchronously. + * Core implementation of -displaysAsynchronously. * Must be called with _propertyLock held. */ - (BOOL)_displaysAsynchronously @@ -558,16 +652,58 @@ void ASDisplayNodePerformBlockOnMainThread(void (^block)()) [[self asyncLayer] displayImmediately]; } +- (void)__setNeedsLayout +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + + if (!_flags.isMeasured) { + return; + } + + ASSizeRange oldConstrainedSize = _constrainedSize; + [self invalidateCalculatedLayout]; + + if (_supernode) { + // Cause supernode's layout to be invalidated + [_supernode setNeedsLayout]; + } else { + // This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used. + [self measureWithSizeRange:oldConstrainedSize]; + + CGSize oldSize = self.bounds.size; + CGSize newSize = _layout.size; + + if (! CGSizeEqualToSize(oldSize, newSize)) { + CGRect bounds = self.bounds; + bounds.size = newSize; + self.bounds = bounds; + + // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint + // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. + BOOL useLayer = (_layer && ASDisplayNodeThreadIsMain()); + CGPoint anchorPoint = (useLayer ? _layer.anchorPoint : self.anchorPoint); + CGPoint oldPosition = (useLayer ? _layer.position : self.position); + + CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; + CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; + CGPoint newPosition = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); + + useLayer ? _layer.position = newPosition : self.position = newPosition; + } + } +} + // These private methods ensure that subclasses are not required to call super in order for _renderingSubnodes to be properly managed. - (void)__layout { ASDisplayNodeAssertMainThread(); ASDN::MutexLocker l(_propertyLock); - if (CGRectEqualToRect(_layer.bounds, CGRectZero)) { + if (CGRectEqualToRect(self.bounds, CGRectZero)) { return; // Performing layout on a zero-bounds view often results in frame calculations with negative sizes after applying margins, which will cause measureWithSizeRange: on subnodes to assert. } - _placeholderLayer.frame = _layer.bounds; + _placeholderLayer.frame = self.bounds; [self layout]; [self layoutDidFinish]; } @@ -1124,7 +1260,7 @@ static NSInteger incrementIfFound(NSInteger i) { [self willEnterHierarchy]; } _flags.isEnteringHierarchy = NO; - + CALayer *layer = self.layer; if (!self.layer.contents) { [layer setNeedsDisplay]; @@ -1284,33 +1420,36 @@ static NSInteger incrementIfFound(NSInteger i) { - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize { ASDisplayNodeAssertThreadAffinity(self); - if (_methodOverrides & ASDisplayNodeMethodOverrideCalculateSizeThatFits) { - CGSize size = [self calculateSizeThatFits:constrainedSize.max]; - return [ASLayout newWithLayoutableObject:self size:ASSizeRangeClamp(constrainedSize, size)]; - } else { - id layoutSpec = [self layoutSpecThatFits:constrainedSize]; + if (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) { + ASLayoutSpec *layoutSpec = [self layoutSpecThatFits:constrainedSize]; + layoutSpec.isMutable = NO; ASLayout *layout = [layoutSpec measureWithSizeRange:constrainedSize]; // Make sure layoutableObject of the root layout is `self`, so that the flattened layout will be structurally correct. if (layout.layoutableObject != self) { layout.position = CGPointZero; - layout = [ASLayout newWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; + layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; } return [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) { return [_subnodes containsObject:evaluatedLayout.layoutableObject]; }]; + } else { + // If neither -layoutSpecThatFits: nor -calculateSizeThatFits: is overridden by subclassses, preferredFrameSize should be used, + // assume that the default implementation of -calculateSizeThatFits: returns it. + CGSize size = [self calculateSizeThatFits:constrainedSize.max]; + return [ASLayout layoutWithLayoutableObject:self size:ASSizeRangeClamp(constrainedSize, size)]; } } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDisplayNodeAssertThreadAffinity(self); - return CGSizeZero; + return _preferredFrameSize; } -- (id)layoutSpecThatFits:(ASSizeRange)constrainedSize +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { ASDisplayNodeAssertThreadAffinity(self); - return nil; + return [ASLayoutSpec new]; } - (ASLayout *)calculatedLayout @@ -1331,6 +1470,20 @@ static NSInteger incrementIfFound(NSInteger i) { return _constrainedSize; } +- (void)setPreferredFrameSize:(CGSize)preferredFrameSize +{ + ASDN::MutexLocker l(_propertyLock); + if (! CGSizeEqualToSize(_preferredFrameSize, preferredFrameSize)) { + _preferredFrameSize = preferredFrameSize; + [self invalidateCalculatedLayout]; + } +} + +- (CGSize)preferredFrameSize +{ + ASDN::MutexLocker l(_propertyLock); + return _preferredFrameSize; +} - (UIImage *)placeholderImage { return nil; @@ -1343,6 +1496,15 @@ static NSInteger incrementIfFound(NSInteger i) { _flags.isMeasured = NO; } +- (void)__didLoad +{ + if (_nodeLoadedBlock) { + _nodeLoadedBlock(self); + _nodeLoadedBlock = nil; + } + [self didLoad]; +} + - (void)didLoad { ASDisplayNodeAssertMainThread(); @@ -1408,11 +1570,11 @@ static NSInteger incrementIfFound(NSInteger i) { - (void)layout { ASDisplayNodeAssertMainThread(); - + if (!_flags.isMeasured) { return; } - + // Assume that _layout was flattened and is 1-level deep. for (ASLayout *subnodeLayout in _layout.sublayouts) { ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Cached sublayouts must only contain subnodes' layout."); @@ -1771,6 +1933,11 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, return !self.layerBacked && [self.view canPerformAction:action withSender:sender]; } +- (id)finalLayoutable +{ + return self; +} + @end @implementation ASDisplayNode (Debugging) diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index bb042be975..5ce6ac2ba5 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -60,6 +60,7 @@ extern id ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDi extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c); extern UIColor *ASDisplayNodeDefaultPlaceholderColor(); +extern UIColor *ASDisplayNodeDefaultTintColor(); /** Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported. diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index b8a4c1a4eb..8d2e261570 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -126,7 +126,24 @@ extern id ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) UIColor *ASDisplayNodeDefaultPlaceholderColor() { - return [UIColor colorWithWhite:0.95 alpha:1.0]; + static UIColor *defaultPlaceholderColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultPlaceholderColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + }); + return defaultPlaceholderColor; +} + +UIColor *ASDisplayNodeDefaultTintColor() +{ + static UIColor *defaultTintColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0]; + }); + return defaultTintColor; } #pragma mark - Hierarchy Notifications diff --git a/AsyncDisplayKit/ASEditableTextNode.h b/AsyncDisplayKit/ASEditableTextNode.h index 92811e2457..e343b4b420 100644 --- a/AsyncDisplayKit/ASEditableTextNode.h +++ b/AsyncDisplayKit/ASEditableTextNode.h @@ -11,14 +11,20 @@ @protocol ASEditableTextNodeDelegate; -//! @abstract ASEditableTextNode implements a node that supports text editing. +/// @abstract ASEditableTextNode implements a node that supports text editing. @interface ASEditableTextNode : ASDisplayNode -//! @abstract The text node's delegate, which must conform to the protocol. +// @abstract The text node's delegate, which must conform to the protocol. @property (nonatomic, readwrite, weak) id delegate; #pragma mark - Configuration +/** + @abstract Access to underlying UITextView for more configuration options. + @warning This property should only be used on the main thread and should not be accessed before the editable text node's view is created. + */ +@property (nonatomic, readonly, strong) UITextView *textView; + //! @abstract The attributes to apply to new text being entered by the user. @property (nonatomic, readwrite, strong) NSDictionary *typingAttributes; @@ -50,6 +56,11 @@ //! @abstract The text input mode used by the receiver's keyboard, if it is visible. This value is undefined if the receiver is not the first responder. @property (nonatomic, readonly) UITextInputMode *textInputMode; +/* + @abstract The returnKeyType of the keyboard. This value defaults to UIReturnKeyDefault. + */ +@property (nonatomic, readwrite) UIReturnKeyType returnKeyType; + /** @abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user. @result YES if the receiver's text view is the first-responder; NO otherwise. @@ -74,6 +85,11 @@ @end #pragma mark - +/** + * The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to + * respond to notifications such as began and finished editing, selection changed and text updated; + * and manage whether a specified text should be replaced. + */ @protocol ASEditableTextNodeDelegate @optional diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index 3761134481..c2ecde2b95 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -77,6 +77,7 @@ _textKitComponents = [ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero]; _textKitComponents.layoutManager.delegate = self; _wordKerner = [[ASTextNodeWordKerner alloc] init]; + _returnKeyType = UIReturnKeyDefault; // Create the placeholder scaffolding. _placeholderTextKitComponents = [ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero]; @@ -85,13 +86,13 @@ return self; } -- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; } -- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; @@ -133,11 +134,12 @@ [self.view addSubview:_placeholderTextKitComponents.textView]; // Create and configure our text view. - _textKitComponents.textView = [[_ASDisabledPanUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer]; + _textKitComponents.textView = self.textView; //_textKitComponents.textView = NO; // Unfortunately there's a bug here with iOS 7 DP5 that causes the text-view to only be one line high when scrollEnabled is NO. rdar://14729288 _textKitComponents.textView.delegate = self; _textKitComponents.textView.editable = YES; _textKitComponents.textView.typingAttributes = _typingAttributes; + _textKitComponents.textView.returnKeyType = _returnKeyType; _textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string; configureTextView(_textKitComponents.textView); [self.view addSubview:_textKitComponents.textView]; @@ -188,6 +190,15 @@ #pragma mark - Configuration @synthesize delegate = _delegate; +- (UITextView *)textView +{ + ASDisplayNodeAssertMainThread(); + if (!_textKitComponents.textView) { + _textKitComponents.textView = [[_ASDisabledPanUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer]; + } + return _textKitComponents.textView; +} + #pragma mark - @dynamic typingAttributes; @@ -346,6 +357,13 @@ return [_textKitComponents.textView textInputMode]; } +- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType +{ + ASDN::MutexLocker l(_textKitLock); + _returnKeyType = returnKeyType; + [_textKitComponents.textView setReturnKeyType:_returnKeyType]; +} + - (BOOL)isFirstResponder { ASDN::MutexLocker l(_textKitLock); diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index d7cdebbd94..dce8d0f076 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -96,13 +96,13 @@ return self; } -- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; } -- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; @@ -111,7 +111,10 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDN::MutexLocker l(_imageLock); - if (_image) + // if a preferredFrameSize is set, call the superclass to return that instead of using the image size. + if (CGSizeEqualToSize(self.preferredFrameSize, CGSizeZero) == NO) + return [super calculateSizeThatFits:constrainedSize]; + else if (_image) return _image.size; else return CGSizeZero; diff --git a/AsyncDisplayKit/ASMultiplexImageNode.h b/AsyncDisplayKit/ASMultiplexImageNode.h index c62f7c3153..db10daa324 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.h +++ b/AsyncDisplayKit/ASMultiplexImageNode.h @@ -102,6 +102,10 @@ typedef NS_ENUM(NSUInteger, ASMultiplexImageNodeErrorCode) { #pragma mark - +/** + * The methods declared by the ASMultiplexImageNodeDelegate protocol allow the adopting delegate to respond to + * notifications such as began, progressed and finished downloading, updated and displayed an image. + */ @protocol ASMultiplexImageNodeDelegate @optional @@ -170,6 +174,10 @@ didFinishDownloadingImageWithIdentifier:(id)imageIdentifier #pragma mark - +/** + * The ASMultiplexImageNodeDataSource protocol is adopted by an object that provides the multiplex image node, + * for each image identifier, an image or a URL the image node should load. + */ @protocol ASMultiplexImageNodeDataSource @optional diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 0bc9c6e8f8..d32d3be061 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -139,6 +139,13 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent */ - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL completion:(void (^)(UIImage *image, NSError *error))completionBlock; +/** + @abstract Returns a Boolean value indicating whether the downloaded image should be removed when clearing fetched data + @discussion Downloaded image data should only be cleared out if a cache is present + @return YES if an image cache is available; otherwise, NO. + */ +- (BOOL)_shouldClearFetchedImageData; + @end @implementation ASMultiplexImageNode @@ -173,11 +180,15 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent } } -- (void)displayWillStart +- (void)clearFetchedData { - [super displayWillStart]; - - [self fetchData]; + [super clearFetchedData]; + + if ([self _shouldClearFetchedImageData]) { + // setting this to nil makes the node fetch images the next time its display starts + _loadedImageIdentifier = nil; + self.image = nil; + } } - (void)fetchData @@ -626,4 +637,8 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent [self _loadNextImage]; } +- (BOOL)_shouldClearFetchedImageData { + return _cache != nil; +} + @end diff --git a/AsyncDisplayKit/ASNetworkImageNode.h b/AsyncDisplayKit/ASNetworkImageNode.h index 7039004c95..5dc723521e 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.h +++ b/AsyncDisplayKit/ASNetworkImageNode.h @@ -61,6 +61,8 @@ /** * Download and display a new image. * + * @param URL The URL of a new image to download and display. + * * @param reset Whether to display a placeholder () while loading the new image. */ - (void)setURL:(NSURL *)URL resetToDefault:(BOOL)reset; @@ -74,6 +76,10 @@ #pragma mark - +/** + * The methods declared by the ASNetworkImageNodeDelegate protocol allow the adopting delegate to respond to + * notifications such as fininished decoding and downloading an image. + */ @protocol ASNetworkImageNodeDelegate /** diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index 8b399a01b4..ed3ecd6212 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -51,7 +51,14 @@ - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeType:(ASLayoutRangeType)rangeType; /** - * initializer. + * Initializer. + * + * @param frame A rectangle specifying the initial location and size of the table view in its superview’€™s coordinates. + * The frame of the table view changes as table cells are added and deleted. + * + * @param style A constant that specifies the style of the table view. See UITableViewStyle for descriptions of valid constants. + * + * @param asyncDataFetchingEnabled Enable the data fetching in async mode. * * @discussion If asyncDataFetching is enabled, the `ASTableView` will fetch data through `tableView:numberOfRowsInSection:` and * `tableView:nodeForRowAtIndexPath:` in async mode from background thread. Otherwise, the methods will be invoked synchronically @@ -86,35 +93,129 @@ */ - (void)reloadData; - /** - * We don't support the these methods for animation yet. - * - * TODO: support animations. + * begins a batch of insert, delete reload and move operations. This method must be called from the main thread. */ - (void)beginUpdates; + +/** + * Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view, with animation enabled and no completion block. + * You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. This method is must be called from the main thread. It's important to remeber that the ASTableView will + * be processing the updates asynchronously after this call is completed. + */ - (void)endUpdates; /** - * Section updating. + * Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view. + * You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations + * to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating + * the operations simultaneously. This method is must be called from the main thread. It's important to remeber that the ASTableView will + * be processing the updates asynchronously after this call and are not guaranteed to be reflected in the ASTableView until + * the completion block is executed. * - * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection - * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + * @param animated NO to disable all animations. + * @param completion A completion handler block to execute when all of the operations are finished. This block takes a single + * Boolean parameter that contains the value YES if all of the related animations completed successfully or + * NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread. + */ +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion; + +/** + * Inserts one or more sections, with an option to animate the insertion. + * + * @param sections An index set that specifies the sections to insert. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. */ - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes one or more sections, with an option to animate the deletion. + * + * @param sections An index set that specifies the sections to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified sections using a given animation effect. + * + * @param sections An index set that specifies the sections to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves a section to a new location. + * + * @param section The index of the section to move. + * + * @param newSection The index that is the destination of the move for the section. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; /** - * Row updating. + * Inserts rows at the locations identified by an array of index paths, with an option to animate the insertion. * - * All operations are asynchronous and thread safe. You can call it from background thread (it is recommendated) and the UI collection - * view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before these methods are called. + * @param indexPaths An array of NSIndexPath objects, each representing a row index and section index that together identify a row. + * + * @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. */ - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Deletes the rows specified by an array of index paths, with an option to animate the deletion. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to delete. + * + * @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Reloads the specified rows using a given animation effect. + * + * @param indexPaths An array of NSIndexPath objects identifying the rows to reload. + * + * @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation; + +/** + * Moves the row at a specified location to a destination location. + * + * @param indexPath The index path identifying the row to move. + * + * @param newIndexPath The index path that is the destination of the move for the row. + * + * @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes + * before this method is called. + */ - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; /** @@ -133,6 +234,15 @@ */ - (NSArray *)visibleNodes; +/** + * YES to automatically adjust the contentOffset when cells are inserted or deleted "before" + * visible cells, maintaining the users' visible scroll position. Currently this feature tracks insertions, moves and deletions of + * cells, but section edits are ignored. + * + * default is NO. + */ +@property (nonatomic) BOOL automaticallyAdjustsContentOffset; + @end diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index b5ff1fb48f..b7ebe9c2f1 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -15,7 +15,11 @@ #import "ASRangeController.h" #import "ASDisplayNodeInternal.h" #import "ASBatchFetching.h" +#import "ASInternalHelpers.h" +#import "ASLayout.h" +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) #pragma mark - #pragma mark Proxying. @@ -101,18 +105,40 @@ static BOOL _isInterceptedSelector(SEL sel) #pragma mark - #pragma mark ASCellNode<->UITableViewCell bridging. +@class _ASTableViewCell; + +@protocol _ASTableViewCellDelegate +- (void)willLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell; +@end + @interface _ASTableViewCell : UITableViewCell +@property (nonatomic, weak) id<_ASTableViewCellDelegate> delegate; +@property (nonatomic, weak) ASCellNode *node; @end @implementation _ASTableViewCell // TODO add assertions to prevent use of view-backed UITableViewCell properties (eg .textLabel) + +- (void)layoutSubviews +{ + [_delegate willLayoutSubviewsOfTableViewCell:self]; + [super layoutSubviews]; +} + +- (void)didTransitionToState:(UITableViewCellStateMask)state +{ + [self setNeedsLayout]; + [self layoutIfNeeded]; + [super didTransitionToState:state]; +} + @end #pragma mark - #pragma mark ASTableView -@interface ASTableView () { +@interface ASTableView () { _ASTableViewProxy *_proxyDataSource; _ASTableViewProxy *_proxyDelegate; @@ -126,6 +152,12 @@ static BOOL _isInterceptedSelector(SEL sel) ASBatchContext *_batchContext; NSIndexPath *_pendingVisibleIndexPath; + + NSIndexPath *_contentOffsetAdjustmentTopVisibleRow; + CGFloat _contentOffsetAdjustment; + + CGFloat _maxWidthForNodesConstrainedSize; + BOOL _ignoreMaxWidthChange; } @property (atomic, assign) BOOL asyncDataSourceLocked; @@ -176,6 +208,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _leadingScreensForBatching = 1.0; _batchContext = [[ASBatchContext alloc] init]; + + _automaticallyAdjustsContentOffset = NO; + + _maxWidthForNodesConstrainedSize = self.bounds.size.width; + // 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. + _ignoreMaxWidthChange = (_maxWidthForNodesConstrainedSize == 0); } - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style @@ -321,58 +360,143 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)beginUpdates { + ASDisplayNodeAssertMainThread(); [_dataController beginUpdates]; } - (void)endUpdates { - [_dataController endUpdates]; + [self endUpdatesAnimated:YES completion:nil]; } +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion; +{ + ASDisplayNodeAssertMainThread(); + [_dataController endUpdatesAnimated:animated completion:completion]; +} + +- (void)layoutSubviews +{ + if (_maxWidthForNodesConstrainedSize != self.bounds.size.width) { + _maxWidthForNodesConstrainedSize = self.bounds.size.width; + + // First width change occurs during initial configuration. An expensive relayout pass is unnecessary at that time + // and should be avoided, assuming that the initial data loading automatically runs shortly afterward. + if (_ignoreMaxWidthChange) { + _ignoreMaxWidthChange = NO; + } else { + [self beginUpdates]; + [_dataController relayoutAllRows]; + [self endUpdates]; + } + } + + // To ensure _maxWidthForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last + [super layoutSubviews]; +} #pragma mark - #pragma mark Editing - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController insertSections:sections withAnimationOptions:animation]; } - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController deleteSections:sections withAnimationOptions:animation]; } - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController reloadSections:sections withAnimationOptions:animation]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { + ASDisplayNodeAssertMainThread(); [_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone]; } - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { + ASDisplayNodeAssertMainThread(); [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { + ASDisplayNodeAssertMainThread(); [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone]; } +#pragma mark - +#pragma mark adjust content offset + +- (void)beginAdjustingContentOffset +{ + ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES"); + _contentOffsetAdjustment = 0; + _contentOffsetAdjustmentTopVisibleRow = self.indexPathsForVisibleRows.firstObject; +} + +- (void)endAdjustingContentOffset +{ + ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES"); + if (_contentOffsetAdjustment != 0) { + self.contentOffset = CGPointMake(0, self.contentOffset.y+_contentOffsetAdjustment); + } + + _contentOffsetAdjustment = 0; + _contentOffsetAdjustmentTopVisibleRow = nil; +} + +- (void)adjustContentOffsetWithNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths inserting:(BOOL)inserting { + // Maintain the users visible window when inserting or deleteing cells by adjusting the content offset for nodes + // before the visible area. If in a begin/end updates block this will update _contentOffsetAdjustment, otherwise it will + // update self.contentOffset directly. + + ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES"); + + CGFloat dir = (inserting) ? +1 : -1; + CGFloat adjustment = 0; + NSIndexPath *top = _contentOffsetAdjustmentTopVisibleRow ?: self.indexPathsForVisibleRows.firstObject; + + for (int index = 0; index < indexPaths.count; index++) { + NSIndexPath *indexPath = indexPaths[index]; + if ([indexPath compare:top] <= 0) { // if this row is before or equal to the topmost visible row, make adjustments... + ASCellNode *cellNode = nodes[index]; + adjustment += cellNode.calculatedSize.height * dir; + if (indexPath.section == top.section) { + top = [NSIndexPath indexPathForRow:top.row+dir inSection:top.section]; + } + } + } + + if (_contentOffsetAdjustmentTopVisibleRow) { // true of we are in a begin/end update block (see beginAdjustingContentOffset) + _contentOffsetAdjustmentTopVisibleRow = top; + _contentOffsetAdjustment += adjustment; + } else if (adjustment != 0) { + self.contentOffset = CGPointMake(0, self.contentOffset.y+adjustment); + } +} + #pragma mark - #pragma mark Intercepted selectors @@ -383,11 +507,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _ASTableViewCell *cell = [self dequeueReusableCellWithIdentifier:reuseIdentifier]; if (!cell) { cell = [[_ASTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; + cell.delegate = self; } ASCellNode *node = [_dataController nodeAtIndexPath:indexPath]; [_rangeController configureContentView:cell.contentView forCellNode:node]; + cell.node = node; cell.backgroundColor = node.backgroundColor; cell.selectionStyle = node.selectionStyle; @@ -499,13 +625,38 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); + LOG(@"--- UITableView beginUpdates"); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + [super beginUpdates]; + + if (_automaticallyAdjustsContentOffset) { + [self beginAdjustingContentOffset]; + } } -- (void)rangeControllerEndUpdates:(ASRangeController *)rangeController completion:(void (^)(BOOL))completion +- (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { ASDisplayNodeAssertMainThread(); - [super endUpdates]; + LOG(@"--- UITableView endUpdates"); + + if (!self.asyncDataSource) { + if (completion) { + completion(NO); + } + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } + + if (_automaticallyAdjustsContentOffset) { + [self endAdjustingContentOffset]; + } + + ASPerformBlockWithoutAnimation(!animated, ^{ + [super endUpdates]; + }); if (completion) { completion(YES); @@ -573,29 +724,53 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { return self.bounds.size; } -- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + LOG(@"UITableView insertRows:%ld rows", indexPaths.count); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; }); + + if (_automaticallyAdjustsContentOffset) { + [self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES]; + } } -- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + LOG(@"UITableView deleteRows:%ld rows", indexPaths.count); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; }); + + if (_automaticallyAdjustsContentOffset) { + [self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:NO]; + } } - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + LOG(@"UITableView insertSections:%@", indexSet); + + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ @@ -606,6 +781,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); + LOG(@"UITableView deleteSections:%@", indexSet); + + if (!self.asyncDataSource) { + return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes + } BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ @@ -622,9 +802,10 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { return node; } -- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { - return CGSizeMake(self.bounds.size.width, FLT_MAX); + return ASSizeRangeMake(CGSizeMake(_maxWidthForNodesConstrainedSize, 0), + CGSizeMake(_maxWidthForNodesConstrainedSize, FLT_MAX)); } - (void)dataControllerLockDataSource @@ -663,4 +844,33 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { } } +#pragma mark - _ASTableViewCellDelegate + +- (void)willLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell +{ + CGFloat contentViewWidth = tableViewCell.contentView.bounds.size.width; + ASCellNode *node = tableViewCell.node; + ASSizeRange constrainedSize = node.constrainedSizeForCalculatedLayout; + + // Table view cells should always fill its content view width. + // Normally the content view width equals to the constrained size width (which equals to the table view width). + // If there is a mismatch between these values, for example after the table view entered or left editing mode, + // content view width is preferred and used to re-measure the cell node. + if (contentViewWidth != constrainedSize.max.width) { + constrainedSize.min.width = contentViewWidth; + constrainedSize.max.width = contentViewWidth; + + // Re-measurement is done on main to ensure thread affinity. In the worst case, this is as fast as UIKit's implementation. + // + // Unloaded nodes *could* be re-measured off the main thread, but only with the assumption that content view width + // is the same for all cells (because there is no easy way to get that individual value before the node being assigned to a _ASTableViewCell). + // Also, in many cases, some nodes may not need to be re-measured at all, such as when user enters and then immediately leaves editing mode. + // To avoid premature optimization and making such assumption, as well as to keep ASTableView simple, re-measurement is strictly done on main. + [self beginUpdates]; + CGSize calculatedSize = [[node measureWithSizeRange:constrainedSize] size]; + node.frame = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height); + [self endUpdates]; + } +} + @end diff --git a/AsyncDisplayKit/ASTextNode.h b/AsyncDisplayKit/ASTextNode.h index fc520c1232..e8d5b76154 100644 --- a/AsyncDisplayKit/ASTextNode.h +++ b/AsyncDisplayKit/ASTextNode.h @@ -8,7 +8,6 @@ #import - @protocol ASTextNodeDelegate; /** diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 0206040c07..d688de0deb 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -11,6 +11,7 @@ #import #import #import +#import #import #import #import @@ -125,7 +126,7 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) self.needsDisplayOnBoundsChange = YES; _truncationMode = NSLineBreakByWordWrapping; - _truncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; + _truncationAttributedString = DefaultTruncationAttributedString(); // The common case is for a text node to be non-opaque and blended over some background. self.opaque = NO; @@ -148,13 +149,13 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) return self; } -- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; } -- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { ASDisplayNodeAssertNotSupported(); return nil; @@ -202,7 +203,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) _constrainedSize = constrainedSizeForText; [self _invalidateRenderer]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); CGSize rendererSize = [[self _renderer] size]; // Add shadow padding back @@ -337,19 +340,21 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) // We need an entirely new renderer [self _invalidateRenderer]; - // Tell the display node superclasses that the cached layout is incorrect now - [self invalidateCalculatedLayout]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + // Tell the display node superclasses that the cached layout is incorrect now + [self invalidateCalculatedLayout]; - [self setNeedsDisplay]; + [self setNeedsDisplay]; - self.accessibilityLabel = _attributedString.string; + self.accessibilityLabel = _attributedString.string; - if (_attributedString.length == 0) { - // We're not an accessibility element by default if there is no string. - self.isAccessibilityElement = NO; - } else { - self.isAccessibilityElement = YES; - } + if (_attributedString.length == 0) { + // We're not an accessibility element by default if there is no string. + self.isAccessibilityElement = NO; + } else { + self.isAccessibilityElement = YES; + } + }); } #pragma mark - Text Layout @@ -360,7 +365,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) _exclusionPaths = exclusionPaths; [self _invalidateRenderer]; [self invalidateCalculatedLayout]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -897,7 +904,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) } _shadowColor = shadowColor; [self _invalidateShadower]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -911,7 +920,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; [self _invalidateShadower]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -925,7 +936,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; [self _invalidateShadower]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -939,7 +952,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; [self _invalidateShadower]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -950,6 +965,16 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) #pragma mark - Truncation Message +static NSAttributedString *DefaultTruncationAttributedString() +{ + static NSAttributedString *defaultTruncationAttributedString; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTruncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; + }); + return defaultTruncationAttributedString; +} + - (void)setTruncationAttributedString:(NSAttributedString *)truncationAttributedString { // No-op if they're exactly equal (avoid redrawing) @@ -981,7 +1006,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) if (_truncationMode != truncationMode) { _truncationMode = truncationMode; [self _invalidateRenderer]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } } @@ -994,8 +1021,10 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) { if (_maximumLineCount != maximumLineCount) { _maximumLineCount = maximumLineCount; - [self _invalidateRenderer]; + [self _invalidateRenderer]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ [self setNeedsDisplay]; + }); } } @@ -1010,7 +1039,9 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) { _composedTruncationString = [self _prepareTruncationStringForDrawing:[self _composedTruncationString]]; [self _invalidateRenderer]; - [self setNeedsDisplay]; + ASDisplayNodeRespectThreadAffinityOfNode(self, ^{ + [self setNeedsDisplay]; + }); } /** diff --git a/AsyncDisplayKit/AsyncDisplayKit.h b/AsyncDisplayKit/AsyncDisplayKit.h index 46b3677f1b..e4ba08c8f4 100644 --- a/AsyncDisplayKit/AsyncDisplayKit.h +++ b/AsyncDisplayKit/AsyncDisplayKit.h @@ -21,6 +21,7 @@ #import #import +#import #import #import @@ -35,5 +36,5 @@ #import #import #import -#import +#import #import diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index 8655102738..cbed7f2bb7 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -13,6 +13,7 @@ #import "ASAssert.h" #import "ASCollectionView.h" #import "CGRect+ASConvenience.h" +#import "UICollectionViewLayout+ASConvenience.h" struct ASDirectionalScreenfulBuffer { CGFloat positiveDirection; // Positive relative to iOS Core Animation layer coordinate space. @@ -56,7 +57,7 @@ typedef struct ASRangeGeometry ASRangeGeometry; @interface ASCollectionViewLayoutController () { - UIScrollView * __weak _scrollView; + ASCollectionView * __weak _collectionView; UICollectionViewLayout * __strong _collectionViewLayout; std::vector _updateRangeBoundsIndexedByRangeType; ASScrollDirection _scrollableDirections; @@ -72,7 +73,7 @@ typedef struct ASRangeGeometry ASRangeGeometry; } _scrollableDirections = [collectionView scrollableDirections]; - _scrollView = collectionView; + _collectionView = collectionView; _collectionViewLayout = [collectionView collectionViewLayout]; _updateRangeBoundsIndexedByRangeType = std::vector(ASLayoutRangeTypeCount); return self; @@ -94,8 +95,13 @@ typedef struct ASRangeGeometry ASRangeGeometry; - (ASRangeGeometry)rangeGeometryWithScrollDirection:(ASScrollDirection)scrollDirection rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters { - CGRect rangeBounds = _scrollView.bounds; - CGRect updateBounds = _scrollView.bounds; + CGRect rangeBounds = _collectionView.bounds; + CGRect updateBounds = _collectionView.bounds; + + //scrollable directions can change for non-flow layouts + if ([_collectionViewLayout asdk_isFlowLayout] == NO) { + _scrollableDirections = [_collectionView scrollableDirections]; + } BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(_scrollableDirections); if (canScrollHorizontally) { @@ -148,7 +154,7 @@ typedef struct ASRangeGeometry ASRangeGeometry; return YES; } - CGRect currentBounds = _scrollView.bounds; + CGRect currentBounds = _collectionView.bounds; if (CGRectIsEmpty(currentBounds)) { currentBounds = CGRectMake(0, 0, viewportSize.width, viewportSize.height); } diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index 08dfd4fbf2..68e586daa8 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -8,6 +8,7 @@ #import #import +#import #import "ASFlowLayoutController.h" @class ASCellNode; @@ -27,9 +28,9 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (ASCellNode *)dataController:(ASDataController *)dataController nodeAtIndexPath:(NSIndexPath *)indexPath; /** - The constrained size for layout. + The constrained size range for layout. */ -- (CGSize)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; +- (ASSizeRange)dataController:(ASDataController *)dataController constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath; /** Fetch the number of rows in specific section. @@ -65,7 +66,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; Called for batch update. */ - (void)dataControllerBeginUpdates:(ASDataController *)dataController; -- (void)dataControllerEndUpdates:(ASDataController *)dataController completion:(void (^)(BOOL))completion; +- (void)dataController:(ASDataController *)dataController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion; /** Called for insertion of elements. @@ -75,7 +76,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** Called for deletion of elements. */ -- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** Called for insertion of sections. @@ -111,7 +112,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; @property (nonatomic, weak) id delegate; /** - * Designated iniailizer. + * Designated initializer. * * @param asyncDataFetchingEnabled Enable the data fetching in async mode. * @@ -138,7 +139,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (void)endUpdates; -- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion; +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion; - (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -154,6 +155,12 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +/** + * Re-measures all loaded nodes. Used to respond to a change in size of the containing view + * (e.g. ASTableView or ASCollectionView after an orientation change). + */ +- (void)relayoutAllRows; + - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index c3d1a916c6..b0e7a94bb5 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -16,6 +16,9 @@ #import "ASMultidimensionalArrayUtils.h" #import "ASDisplayNodeInternal.h" +//#define LOG(...) NSLog(__VA_ARGS__) +#define LOG(...) + const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; static void *kASSizingQueueContext = &kASSizingQueueContext; @@ -73,7 +76,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later. _delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)]; - _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)]; + _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodes:atIndexPaths:withAnimationOptions:)]; _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; } @@ -105,17 +108,19 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; for (NSUInteger j = 0; j < nodes.count && j < indexPaths.count; j += kASDataControllerSizingCountPerProcessor) { NSArray *subIndexPaths = [indexPaths subarrayWithRange:NSMakeRange(j, MIN(kASDataControllerSizingCountPerProcessor, indexPaths.count - j))]; - // TODO: The current implementation does not make use of different constrained sizes per node. - // There should be a fast-path that avoids all of this object creation. + //TODO: There should be a fast-path that avoids all of this object creation. NSMutableArray *nodeBoundSizes = [[NSMutableArray alloc] initWithCapacity:kASDataControllerSizingCountPerProcessor]; [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [nodeBoundSizes addObject:[NSValue valueWithCGSize:[_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]]]; + ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; + [nodeBoundSizes addObject:[NSValue valueWithBytes:&constrainedSize objCType:@encode(ASSizeRange)]]; }]; dispatch_group_async(layoutGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [subIndexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { ASCellNode *node = nodes[j + idx]; - [node measure:[nodeBoundSizes[idx] CGSizeValue]]; + ASSizeRange constrainedSize; + [nodeBoundSizes[idx] getValue:&constrainedSize]; + [node measureWithSizeRange:constrainedSize]; node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); }]; }); @@ -164,12 +169,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { if (indexPaths.count == 0) return; + LOG(@"_deleteNodesAtIndexPaths:%@, full index paths in _editingNodes = %@", indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); ASDisplayNodePerformBlockOnMainThread(^{ + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); } @@ -242,6 +249,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - reloadData"); + // Remove everything that existed before the reload, now that we're ready to insert replacements NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -318,32 +327,46 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)beginUpdates { + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; // Begin queuing up edit calls that happen on the main thread. // This will prevent further operations from being scheduled on _editingTransactionQueue. - // It's fine if there is an in-flight operation on _editingTransactionQueue, - // as once the command queue is unpaused, each edit command will wait for the _editingTransactionQueue to be flushed. _batchUpdateCounter++; } - (void)endUpdates { - [self endUpdatesWithCompletion:NULL]; + [self endUpdatesAnimated:YES completion:nil]; } -- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion +- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { _batchUpdateCounter--; if (_batchUpdateCounter == 0) { - [_delegate dataControllerBeginUpdates:self]; + LOG(@"endUpdatesWithCompletion - beginning"); + + [_editingTransactionQueue addOperationWithBlock:^{ + ASDisplayNodePerformBlockOnMainThread(^{ + LOG(@"endUpdatesWithCompletion - begin updates call to delegate"); + [_delegate dataControllerBeginUpdates:self]; + }); + }]; + // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction. + LOG(@"endUpdatesWithCompletion - %zd blocks to run", _pendingEditCommandBlocks.count); [_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { + LOG(@"endUpdatesWithCompletion - running block #%zd", idx); block(); }]; [_pendingEditCommandBlocks removeAllObjects]; - - [_delegate dataControllerEndUpdates:self completion:completion]; + + [_editingTransactionQueue addOperationWithBlock:^{ + ASDisplayNodePerformBlockOnMainThread(^{ + LOG(@"endUpdatesWithCompletion - calling delegate end"); + [_delegate dataController:self endUpdatesAnimated:animated completion:completion]; + }); + }]; } } @@ -364,6 +387,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - insertSections: %@", indexSet); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ @@ -372,6 +396,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - insertSections: %@", indexSet); NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; for (NSUInteger i = 0; i < indexSet.count; i++) { [sectionArray addObject:[NSMutableArray array]]; @@ -388,10 +413,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - deleteSections: %@", indexSet); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ // remove elements + LOG(@"Edit Transaction - deleteSections: %@", indexSet); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -404,6 +431,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - reloadSections: %@", sections); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ @@ -417,6 +446,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; [_editingTransactionQueue addOperationWithBlock:^{ NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); + + LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes)); + [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // reinsert the elements @@ -430,10 +462,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - moveSection"); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ // remove elements + + LOG(@"Edit Transaction - moveSection"); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -457,6 +494,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - insertRows: %@", indexPaths); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [self accessDataSourceWithBlock:^{ @@ -468,6 +507,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; } [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - insertRows: %@", indexPaths); [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; }]; @@ -478,12 +518,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - deleteRows: %@", indexPaths); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; // sort indexPath in order to avoid messing up the index when deleting NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - deleteRows: %@", indexPaths); [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; }]; }]; @@ -493,6 +536,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - reloadRows: %@", indexPaths); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. @@ -504,6 +549,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - reloadRows: %@", indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }]; @@ -511,13 +557,50 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; }]; } +- (void)relayoutAllRows +{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - relayoutRows"); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + void (^relayoutNodesBlock)(NSMutableArray *) = ^void(NSMutableArray *nodes) { + if (!nodes.count) { + return; + } + + [self accessDataSourceWithBlock:^{ + [nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { + [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]; + ASSizeRange constrainedSize = [_dataSource dataController:self constrainedSizeForNodeAtIndexPath:indexPath]; + [node measureWithSizeRange:constrainedSize]; + node.frame = CGRectMake(0.0f, 0.0f, node.calculatedSize.width, node.calculatedSize.height); + }]; + }]; + }]; + }; + + // Can't relayout right away because _completedNodes may not be up-to-date, + // i.e there might be some nodes that were measured using the old constrained size but haven't been added to _completedNodes + // (see _layoutNodes:atIndexPaths:withAnimationOptions:). + [_editingTransactionQueue addOperationWithBlock:^{ + ASDisplayNodePerformBlockOnMainThread(^{ + relayoutNodesBlock(_completedNodes); + }); + }]; + }]; +} + - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); + LOG(@"Edit Command - moveRow: %@ > %@", indexPath, newIndexPath); [_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue addOperationWithBlock:^{ + LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index 52b9e6e880..7fce4c636d 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -103,16 +103,18 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; NSArray *completedNodes = [_dataSource completedNodes]; + + ASIndexPath currPath = startPath; - while (!ASIndexPathEqualToIndexPath(startPath, endPath)) { - [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:startPath]]; - startPath.row++; + while (!ASIndexPathEqualToIndexPath(currPath, endPath)) { + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]]; + currPath.row++; // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. - while (startPath.row >= [(NSArray *)completedNodes[startPath.section] count] && startPath.section < completedNodes.count - 1) { - startPath.row = 0; - startPath.section++; - ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never reach a further section than endPath"); + while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < completedNodes.count - 1) { + currPath.row = 0; + currPath.section++; + ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath"); } } diff --git a/AsyncDisplayKit/Details/ASIndexPath.h b/AsyncDisplayKit/Details/ASIndexPath.h index 9a307817db..a6eb514a4c 100644 --- a/AsyncDisplayKit/Details/ASIndexPath.h +++ b/AsyncDisplayKit/Details/ASIndexPath.h @@ -7,6 +7,7 @@ */ #import +#import typedef struct { NSInteger section; diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index cdfb0568b4..9d5bbe57f0 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -30,6 +30,8 @@ * Notify the range controller that the visible range has been updated. * This is the primary input call that drives updating the working ranges, and triggering their actions. * + * @param scrollDirection The current scroll direction of the scroll view. + * * @see [ASRangeControllerDelegate rangeControllerVisibleNodeIndexPaths:] */ - (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection; @@ -74,36 +76,74 @@ /** * Begin updates. + * + * @param rangeController Sender. */ - (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController; /** * End updates. + * + * @param rangeController Sender. + * @param animated NO if all animations are disabled. YES otherwise. + * @param completion Completion block. */ -- (void)rangeControllerEndUpdates:(ASRangeController * )rangeController completion:(void (^)(BOOL))completion ; +- (void)rangeController:(ASRangeController * )rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion; /** * Fetch nodes at specific index paths. + * + * @param rangeController Sender. + * + * @param indexPaths Index paths. */ - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths; /** * Called for nodes insertion. + * + * @param rangeController Sender. + * + * @param nodes Inserted nodes. + * + * @param indexPaths Index path of inserted nodes. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. */ -- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** * Called for nodes deletion. + * + * @param rangeController Sender. + * + * @param nodes Deleted nodes. + * + * @param indexPaths Index path of deleted nodes. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. */ -- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; +- (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** * Called for section insertion. + * + * @param rangeController Sender. + * + * @param indexSet Index set of inserted sections. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. */ - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; /** * Called for section deletion. + * + * @param rangeController Sender. + * + * @param indexSet Index set of deleted sections. + * + * @param animationOptions Animation options. See ASDataControllerAnimationOptions. */ - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index f41d9d291c..b22db853bf 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -89,6 +89,12 @@ } NSArray *visibleNodePaths = [_delegate rangeControllerVisibleNodeIndexPaths:self]; + + if ( visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... + _queuedRangeUpdate = NO; + return ; // don't do anything for this update, but leave _rangeIsValid to make sure we update it later + } + NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; CGSize viewportSize = [_delegate rangeControllerViewportSize:self]; @@ -104,7 +110,7 @@ // this delegate decide what happens when a node is added or removed from a range id rangeDelegate = _rangeTypeHandlers[rangeKey]; - if ([_layoutController shouldUpdateForVisibleIndexPaths:visibleNodePaths viewportSize:viewportSize rangeType:rangeType]) { + if (!_rangeIsValid || [_layoutController shouldUpdateForVisibleIndexPaths:visibleNodePaths viewportSize:viewportSize rangeType:rangeType]) { NSSet *indexPaths = [_layoutController indexPathsForScrolling:_scrollDirection viewportSize:viewportSize rangeType:rangeType]; // Notify to remove indexpaths that are leftover that are not visible or included in the _layoutController calculated paths @@ -176,9 +182,9 @@ }); } -- (void)dataControllerEndUpdates:(ASDataController *)dataController completion:(void (^)(BOOL))completion { +- (void)dataController:(ASDataController *)dataController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { ASDisplayNodePerformBlockOnMainThread(^{ - [_delegate rangeControllerEndUpdates:self completion:completion]; + [_delegate rangeController:self endUpdatesAnimated:animated completion:completion]; }); } @@ -192,14 +198,14 @@ ASDisplayNodePerformBlockOnMainThread(^{ _rangeIsValid = NO; - [_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [_delegate rangeController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); } -- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { +- (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodePerformBlockOnMainThread(^{ _rangeIsValid = NO; - [_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [_delegate rangeController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); } diff --git a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm index b9cf41c87a..0363abba2f 100644 --- a/AsyncDisplayKit/Details/ASRangeHandlerRender.mm +++ b/AsyncDisplayKit/Details/ASRangeHandlerRender.mm @@ -39,8 +39,12 @@ [node recursivelySetDisplaySuspended:NO]; - // add the node to an off-screen window to force display and preserve its contents - [[self.class workingWindow] addSubnode:node]; + // Add the node's layer to an off-screen window to trigger display and mark its contents as non-volatile. + // Use the layer directly to avoid the substantial overhead of UIView heirarchy manipulations. + // Any view-backed nodes will still create their views in order to assemble the layer heirarchy, and they will + // also assemble a view subtree for the node, but we avoid the much more significant expense triggered by a view + // being added or removed from an onscreen window (responder chain setup, will/DidMoveToWindow: recursive calls, etc) + [[[[self class] workingWindow] layer] addSublayer:node.layer]; } - (void)node:(ASDisplayNode *)node exitedRangeOfType:(ASLayoutRangeType)rangeType @@ -48,9 +52,36 @@ ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(rangeType == ASLayoutRangeTypeRender, @"Render delegate should not handle other ranges"); + // This code is tricky. There are several possible states a node can be in when it reaches this point. + // 1. Layer-backed vs view-backed nodes. AS of this writing, only ASCellNodes arrive here, which are always view-backed — + // but we maintain correctness for all ASDisplayNodes, including layer-backed ones. + // (Note: it would not make sense to pass in a subnode of a rasterized node here, so that is unsupported). + // 2. The node's layer may have been added to the workingWindow previously, or it may have never been added, such as if rangeTuningParameter's leading value is 0. + // 3. The node's layer may not be present in the workingWindow, even if it was previously added. + // This is a common case, as once the node is added to an active cell contentsView (e.g. visible), it is automatically removed from the workingWindow. + // The system does this when addSublayer is called, even if removeFromSuperlayer is never explicitly called. + // 4. Lastly and most unusually, it is possible for a node to be offscreen, completely outside the heirarchy, and yet considered within the working range. + // This happens if the UITableViewCell is reused after scrolling offscreen. Because the node has already been given the opportunity to display, we do not + // proactively re-host it within the workingWindow (improving efficiency). Some time later, it may fall outside the working range, in which case calling + // -recursivelyClearContents is critical. If the user scrolls back and it is re-hosted in a UITableViewCell, the content will still exist as it is not cleared + // by simply being removed from the cell. The code that usually triggers this condition is the -removeFromSuperview in -[ASRangeController configureContentView:forCellNode:]. + // Condition #4 is suboptimal in some cases, as it is conceivable that memory warnings could trigger clearing content that is inside the working range. However, enforcing the + // preservation of this content could result in the app being killed, which is not likely preferable over briefly seeing placeholders in the event the user scrolls backwards. + // Nonetheless, future changes to the implementation will likely eliminate this behavior to simplify debugging and extensibility of working range functionality. + [node recursivelySetDisplaySuspended:YES]; - [node.view removeFromSuperview]; - + + if (node.layer.superlayer != [[[self class] workingWindow] layer]) { + // In this case, the node has previously passed through the working range (or it is zero), and it has now fallen outside the working range. + if (![node isLayerBacked]) { + // If the node is view-backed, we need to make sure to remove the view (which is now present in the containing cell contentsView). + // Layer-backed nodes will be fully handled by the unconditional removal below. + [node.view removeFromSuperview]; + } + } + + // At this point, the node's layer may validly be present either in the workingWindow, or in the contentsView of a cell. + [node.layer removeFromSuperlayer]; [node recursivelyClearContents]; } diff --git a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m index 3ea9d596f2..55efebdce8 100644 --- a/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m +++ b/AsyncDisplayKit/Details/ASTextNodeCoreTextAdditions.m @@ -11,6 +11,8 @@ #import #import +#import "ASAssert.h" + #pragma mark - Public BOOL ASAttributeWithNameIsUnsupportedCoreTextAttribute(NSString *attributeName) { @@ -65,8 +67,13 @@ NSDictionary *NSAttributedStringAttributesForCoreTextAttributes(NSDictionary *co CTFontRef coreTextFont = (__bridge CTFontRef)coreTextValue; NSString *fontName = (__bridge_transfer NSString *)CTFontCopyPostScriptName(coreTextFont); CGFloat fontSize = CTFontGetSize(coreTextFont); - - cleanAttributes[NSFontAttributeName] = [UIFont fontWithName:fontName size:fontSize]; + UIFont *font = [UIFont fontWithName:fontName size:fontSize]; + ASDisplayNodeCAssertNotNil(font, @"unable to load font %@ with size %f", fontName, fontSize); + if (font == nil) { + // Gracefully fail if we were unable to load the font. + font = [UIFont systemFontOfSize:fontSize]; + } + cleanAttributes[NSFontAttributeName] = font; } // kCTKernAttributeName -> NSKernAttributeName else if ([coreTextKey isEqualToString:(NSString *)kCTKernAttributeName]) { diff --git a/AsyncDisplayKit/Details/ASTextNodeRenderer.mm b/AsyncDisplayKit/Details/ASTextNodeRenderer.mm index b17ae2833f..84b290e08e 100644 --- a/AsyncDisplayKit/Details/ASTextNodeRenderer.mm +++ b/AsyncDisplayKit/Details/ASTextNodeRenderer.mm @@ -173,9 +173,13 @@ static const CGFloat ASTextNodeRendererTextCapHeightPadding = 1.3; { ASDN::MutexLocker l(_textKitLock); + if (_attributedString.length == 0) { + _calculatedSize = CGSizeZero; + return; + } + [self _initializeTextKitComponentsIfNeeded]; - // Force glyph generation and layout, which may not have happened yet (and // isn't triggered by -usedRectForTextContainer:). [_layoutManager ensureLayoutForTextContainer:_textContainer]; diff --git a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h index 508f7c8b28..cab51ea8b3 100644 --- a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.h @@ -15,11 +15,13 @@ */ @interface ASBackgroundLayoutSpec : ASLayoutSpec +@property (nonatomic, strong) id background; + /** @param child A child that is laid out to determine the size of this spec. If this is nil, then this method returns nil. @param background A layoutable object that is laid out behind the child. May be nil, in which case the background is omitted. */ -+ (instancetype)newWithChild:(id)child background:(id)background; ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background; @end diff --git a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm index 135fc372f4..bbeac0b882 100644 --- a/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASBackgroundLayoutSpec.mm @@ -12,30 +12,30 @@ #import "ASAssert.h" #import "ASBaseDefines.h" +#import "ASLayout.h" + +static NSString * const kBackgroundChildKey = @"kBackgroundChildKey"; @interface ASBackgroundLayoutSpec () -{ - id _child; - id _background; -} @end @implementation ASBackgroundLayoutSpec -+ (instancetype)newWithChild:(id)child background:(id)background +- (instancetype)initWithChild:(id)child background:(id)background { - if (child == nil) { + if (!(self = [super init])) { return nil; } - ASBackgroundLayoutSpec *spec = [super new]; - spec->_child = child; - spec->_background = background; - return spec; + + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + [self setChild:child]; + self.background = background; + return self; } -+ (instancetype)new ++ (instancetype)backgroundLayoutSpecWithChild:(id)child background:(id)background; { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + return [[self alloc] initWithChild:child background:background]; } /** @@ -43,19 +43,40 @@ */ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - ASLayout *contentsLayout = [_child measureWithSizeRange:constrainedSize]; + ASLayout *contentsLayout = [[self child] measureWithSizeRange:constrainedSize]; NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:2]; - if (_background) { + if (self.background) { // Size background to exactly the same size. - ASLayout *backgroundLayout = [_background measureWithSizeRange:{contentsLayout.size, contentsLayout.size}]; + ASLayout *backgroundLayout = [self.background measureWithSizeRange:{contentsLayout.size, contentsLayout.size}]; backgroundLayout.position = CGPointZero; [sublayouts addObject:backgroundLayout]; } contentsLayout.position = CGPointZero; [sublayouts addObject:contentsLayout]; - return [ASLayout newWithLayoutableObject:self size:contentsLayout.size sublayouts:sublayouts]; + return [ASLayout layoutWithLayoutableObject:self size:contentsLayout.size sublayouts:sublayouts]; +} + +- (void)setBackground:(id)background +{ + [super setChild:background forIdentifier:kBackgroundChildKey]; +} + +- (id)background +{ + return [super childForIdentifier:kBackgroundChildKey]; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); +} + +- (NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASCenterLayoutSpec.h b/AsyncDisplayKit/Layout/ASCenterLayoutSpec.h index 5afc73197a..37ba24e7e7 100644 --- a/AsyncDisplayKit/Layout/ASCenterLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASCenterLayoutSpec.h @@ -10,6 +10,7 @@ #import +/** How the child is centered within the spec. */ typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecCenteringOptions) { /** The child is positioned in {0,0} relatively to the layout bounds */ ASCenterLayoutSpecCenteringNone = 0, @@ -21,6 +22,7 @@ typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecCenteringOptions) { ASCenterLayoutSpecCenteringXY = ASCenterLayoutSpecCenteringX | ASCenterLayoutSpecCenteringY }; +/** How much space the spec will take up. */ typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecSizingOptions) { /** The spec will take up the maximum size possible */ ASCenterLayoutSpecSizingOptionDefault, @@ -35,12 +37,20 @@ typedef NS_OPTIONS(NSUInteger, ASCenterLayoutSpecSizingOptions) { /** Lays out a single layoutable child and position it so that it is centered into the layout bounds. */ @interface ASCenterLayoutSpec : ASLayoutSpec +@property (nonatomic, assign) ASCenterLayoutSpecCenteringOptions centeringOptions; +@property (nonatomic, assign) ASCenterLayoutSpecSizingOptions sizingOptions; + /** - @param centeringOptions, see ASCenterLayoutSpecCenteringOptions. - @param child The child to center. + * Initializer. + * + * @param centeringOptions How the child is centered. + * + * @param sizingOptions How much space will be taken up. + * + * @param child The child to center. */ -+ (instancetype)newWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions - sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions - child:(id)child; ++ (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child; @end diff --git a/AsyncDisplayKit/Layout/ASCenterLayoutSpec.mm b/AsyncDisplayKit/Layout/ASCenterLayoutSpec.mm index 0ef0f4848d..5e81e1f3d2 100644 --- a/AsyncDisplayKit/Layout/ASCenterLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASCenterLayoutSpec.mm @@ -11,30 +11,45 @@ #import "ASCenterLayoutSpec.h" #import "ASInternalHelpers.h" +#import "ASLayout.h" @implementation ASCenterLayoutSpec { ASCenterLayoutSpecCenteringOptions _centeringOptions; ASCenterLayoutSpecSizingOptions _sizingOptions; - id _child; } -+ (instancetype)newWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions - sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions - child:(id)child +- (instancetype)initWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child; { - ASCenterLayoutSpec *spec = [super new]; - if (spec) { - spec->_centeringOptions = centeringOptions; - spec->_sizingOptions = sizingOptions; - spec->_child = child; + if (!(self = [super init])) { + return nil; } - return spec; + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + _centeringOptions = centeringOptions; + _sizingOptions = sizingOptions; + [self setChild:child]; + return self; } -+ (instancetype)new ++ (instancetype)centerLayoutSpecWithCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions + child:(id)child { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + return [[self alloc] initWithCenteringOptions:centeringOptions sizingOptions:sizingOptions child:child]; +} + +- (void)setCenteringOptions:(ASCenterLayoutSpecCenteringOptions)centeringOptions +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _centeringOptions = centeringOptions; +} + +- (void)setSizingOptions:(ASCenterLayoutSpecSizingOptions)sizingOptions +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _sizingOptions = sizingOptions; } - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize @@ -49,7 +64,7 @@ (_centeringOptions & ASCenterLayoutSpecCenteringX) != 0 ? 0 : constrainedSize.min.width, (_centeringOptions & ASCenterLayoutSpecCenteringY) != 0 ? 0 : constrainedSize.min.height, }; - ASLayout *sublayout = [_child measureWithSizeRange:ASSizeRangeMake(minChildSize, constrainedSize.max)]; + ASLayout *sublayout = [self.child measureWithSizeRange:ASSizeRangeMake(minChildSize, constrainedSize.max)]; // If we have an undetermined height or width, use the child size to define the layout // size @@ -72,7 +87,18 @@ ASRoundPixelValue(shouldCenterAlongY ? (size.height - sublayout.size.height) * 0.5f : 0) }; - return [ASLayout newWithLayoutableObject:self size:size sublayouts:@[sublayout]]; + return [ASLayout layoutWithLayoutableObject:self size:size sublayouts:@[sublayout]]; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); +} + +- (NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index 6c11aa7701..590f9486de 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -13,14 +13,11 @@ /** A dimension relative to constraints to be provided in the future. - A RelativeDimension can be one of two types: - - "Points" - Just a number. It will always resolve to exactly this amount. This is the default type. - - "Percent" - Multiplied to a provided parent amount to resolve a final amount. */ typedef NS_ENUM(NSInteger, ASRelativeDimensionType) { + /** Just a number. It will always resolve to exactly this amount. This is the default type. */ ASRelativeDimensionTypePoints, + /** Multiplied to a provided parent amount to resolve a final amount. */ ASRelativeDimensionTypePercent, }; diff --git a/AsyncDisplayKit/Layout/ASInsetLayoutSpec.h b/AsyncDisplayKit/Layout/ASInsetLayoutSpec.h index 7f1b0c9c00..ab0a6f106f 100644 --- a/AsyncDisplayKit/Layout/ASInsetLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASInsetLayoutSpec.h @@ -29,10 +29,12 @@ */ @interface ASInsetLayoutSpec : ASLayoutSpec +@property (nonatomic, assign) UIEdgeInsets insets; + /** @param insets The amount of space to inset on each side. @param child The wrapped child to inset. If nil, this method returns nil. */ -+ (instancetype)newWithInsets:(UIEdgeInsets)insets child:(id)child; ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child; @end diff --git a/AsyncDisplayKit/Layout/ASInsetLayoutSpec.mm b/AsyncDisplayKit/Layout/ASInsetLayoutSpec.mm index c5f3043107..960360f8c4 100644 --- a/AsyncDisplayKit/Layout/ASInsetLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASInsetLayoutSpec.mm @@ -14,11 +14,11 @@ #import "ASBaseDefines.h" #import "ASInternalHelpers.h" +#import "ASLayout.h" @interface ASInsetLayoutSpec () { UIEdgeInsets _insets; - id _child; } @end @@ -42,22 +42,26 @@ static CGFloat centerInset(CGFloat outer, CGFloat inner) @implementation ASInsetLayoutSpec -+ (instancetype)newWithInsets:(UIEdgeInsets)insets child:(id)child +- (instancetype)initWithInsets:(UIEdgeInsets)insets child:(id)child; { - if (child == nil) { + if (!(self = [super init])) { return nil; } - ASInsetLayoutSpec *spec = [super new]; - if (spec) { - spec->_insets = insets; - spec->_child = child; - } - return spec; + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + _insets = insets; + [self setChild:child]; + return self; } -+ (instancetype)new ++ (instancetype)insetLayoutSpecWithInsets:(UIEdgeInsets)insets child:(id)child { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + return [[self alloc] initWithInsets:insets child:child]; +} + +- (void)setInsets:(UIEdgeInsets)insets +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _insets = insets; } /** @@ -84,7 +88,7 @@ static CGFloat centerInset(CGFloat outer, CGFloat inner) MAX(0, constrainedSize.max.height - insetsY), } }; - ASLayout *sublayout = [_child measureWithSizeRange:insetConstrainedSize]; + ASLayout *sublayout = [self.child measureWithSizeRange:insetConstrainedSize]; const CGSize computedSize = ASSizeRangeClamp(constrainedSize, { finite(sublayout.size.width + _insets.left + _insets.right, constrainedSize.max.width), @@ -102,7 +106,18 @@ static CGFloat centerInset(CGFloat outer, CGFloat inner) sublayout.position = CGPointMake(x, y); - return [ASLayout newWithLayoutableObject:self size:computedSize sublayouts:@[sublayout]]; + return [ASLayout layoutWithLayoutableObject:self size:computedSize sublayouts:@[sublayout]]; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); +} + +- (NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASLayout.h b/AsyncDisplayKit/Layout/ASLayout.h index 5bdd2190c8..71892b5996 100644 --- a/AsyncDisplayKit/Layout/ASLayout.h +++ b/AsyncDisplayKit/Layout/ASLayout.h @@ -32,22 +32,48 @@ extern BOOL CGPointIsNull(CGPoint point); */ @property (nonatomic, readonly) NSArray *sublayouts; -+ (instancetype)newWithLayoutableObject:(id)layoutableObject - size:(CGSize)size - position:(CGPoint)position - sublayouts:(NSArray *)sublayouts; +/** + * Initializer. + * + * @param layoutableObject The backing ASLayoutable object. + * + * @param size The size of this layout. + * + * @param position The posiion of this layout within its parent (if available). + * + * @param sublayouts Sublayouts belong to the new layout. + */ ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject + size:(CGSize)size + position:(CGPoint)position + sublayouts:(NSArray *)sublayouts; /** - * Convenience that has CGPointNull position. + * Convenience initializer that has CGPointNull position. + * Best used by ASDisplayNode subclasses that are manually creating a layout for -calculateLayoutThatFits:, + * or for ASLayoutSpec subclasses that are referencing the "self" level in the layout tree, + * or for creating a sublayout of which the position is yet to be determined. + * + * @param layoutableObject The backing ASLayoutable object. + * + * @param size The size of this layout. + * + * @param sublayouts Sublayouts belong to the new layout. */ -+ (instancetype)newWithLayoutableObject:(id)layoutableObject - size:(CGSize)size - sublayouts:(NSArray *)sublayouts; ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject + size:(CGSize)size + sublayouts:(NSArray *)sublayouts; /** - * Convenience that has CGPointNull position and no sublayouts. + * Convenience that has CGPointNull position and no sublayouts. + * Best used for creating a layout that has no sublayouts, and is either a root one + * or a sublayout of which the position is yet to be determined. + * + * @param layoutableObject The backing ASLayoutable object. + * + * @param size The size of this layout. */ -+ (instancetype)newWithLayoutableObject:(id)layoutableObject size:(CGSize)size; ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject size:(CGSize)size; /** diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index 31dd8ebc27..6695a07bde 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -22,10 +22,10 @@ extern BOOL CGPointIsNull(CGPoint point) @implementation ASLayout -+ (instancetype)newWithLayoutableObject:(id)layoutableObject - size:(CGSize)size - position:(CGPoint)position - sublayouts:(NSArray *)sublayouts ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject + size:(CGSize)size + position:(CGPoint)position + sublayouts:(NSArray *)sublayouts { ASDisplayNodeAssert(layoutableObject, @"layoutableObject is required."); for (ASLayout *sublayout in sublayouts) { @@ -42,16 +42,16 @@ extern BOOL CGPointIsNull(CGPoint point) return l; } -+ (instancetype)newWithLayoutableObject:(id)layoutableObject - size:(CGSize)size - sublayouts:(NSArray *)sublayouts ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject + size:(CGSize)size + sublayouts:(NSArray *)sublayouts { - return [self newWithLayoutableObject:layoutableObject size:size position:CGPointNull sublayouts:sublayouts]; + return [self layoutWithLayoutableObject:layoutableObject size:size position:CGPointNull sublayouts:sublayouts]; } -+ (instancetype)newWithLayoutableObject:(id)layoutableObject size:(CGSize)size ++ (instancetype)layoutWithLayoutableObject:(id)layoutableObject size:(CGSize)size { - return [self newWithLayoutableObject:layoutableObject size:size sublayouts:nil]; + return [self layoutWithLayoutableObject:layoutableObject size:size sublayouts:nil]; } - (ASLayout *)flattenedLayoutUsingPredicateBlock:(BOOL (^)(ASLayout *))predicateBlock @@ -76,10 +76,10 @@ extern BOOL CGPointIsNull(CGPoint point) context.visited = YES; if (predicateBlock(context.layout)) { - [flattenedSublayouts addObject:[ASLayout newWithLayoutableObject:context.layout.layoutableObject - size:context.layout.size - position:context.absolutePosition - sublayouts:nil]]; + [flattenedSublayouts addObject:[ASLayout layoutWithLayoutableObject:context.layout.layoutableObject + size:context.layout.size + position:context.absolutePosition + sublayouts:nil]]; } for (ASLayout *sublayout in context.layout.sublayouts) { @@ -88,7 +88,7 @@ extern BOOL CGPointIsNull(CGPoint point) } } - return [ASLayout newWithLayoutableObject:_layoutableObject size:_size sublayouts:flattenedSublayouts]; + return [ASLayout layoutWithLayoutableObject:_layoutableObject size:_size sublayouts:flattenedSublayouts]; } @end diff --git a/AsyncDisplayKit/Layout/ASLayoutOptions.h b/AsyncDisplayKit/Layout/ASLayoutOptions.h new file mode 100644 index 0000000000..74352a34cf --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutOptions.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014-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 +#import + +@protocol ASLayoutable; + +/** + * A store for all of the options defined by ASLayoutSpec subclasses. All implementors of ASLayoutable own a + * ASLayoutOptions. When certain layoutSpecs need option values, they are read from this class. + * + * Unless you wish to create a custom layout spec, ASLayoutOptions can largerly be ignored. Instead you can access + * the layout option properties exposed in ASLayoutable directly, which will set the values in ASLayoutOptions + * behind the scenes. + */ +@interface ASLayoutOptions : NSObject + +/** + * Sets the class name for the ASLayoutOptions subclasses that will be created when a node or layoutSpec's options + * are first accessed. + * + * If you create a custom layoutSpec that includes new options, you will want to subclass ASLayoutOptions to add + * the new layout options for your layoutSpec(s). In order to make sure your subclass is created instead of an + * instance of ASLayoutOptions, call setDefaultLayoutOptionsClass: early in app launch (applicationDidFinishLaunching:) + * with your subclass's class. + * + * @param defaultLayoutOptionsClass The class of ASLayoutOptions that will be lazily created for a node or layout spec. + */ ++ (void)setDefaultLayoutOptionsClass:(Class)defaultLayoutOptionsClass; + +/** + * @return the Class of ASLayoutOptions that will be created for a node or layoutspec. Defaults to [ASLayoutOptions class]; + */ ++ (Class)defaultLayoutOptionsClass; + +#pragma mark - Subclasses should implement these! +/** + * Initializes a new ASLayoutOptions using the given layoutable to assign any intrinsic option values. + * This init function sets a sensible default value for each layout option. If you create a subclass of + * ASLayoutOptions, your subclass should do the same. + * + * @param layoutable The layoutable that will own these options. The layoutable will be used to set any intrinsic + * layoutOptions. For example, if the layoutable is an ASTextNode the ascender/descender values will get set. + * + * @return a new instance of ASLayoutOptions + */ +- (instancetype)initWithLayoutable:(id)layoutable; + +/** + * Copies the values of layoutOptions into self. This is useful when placing a layoutable inside of another. Consider + * an ASTextNode that you want to align to the baseline by putting it in an ASStackLayoutSpec. Before that, you want + * to inset the ASTextNode by placing it in an ASInsetLayoutSpec. An ASInsetLayoutSpec will not have any information + * about the ASTextNode's ascender/descender unless we copy over the layout options from ASTextNode to ASInsetLayoutSpec. + * This is done automatically and should not need to be called directly. It is listed here to make sure that any + * ASLayoutOptions subclass implements the method. + * + * @param layoutOptions The layoutOptions to copy from + */ +- (void)copyIntoOptions:(ASLayoutOptions *)layoutOptions; + +#pragma mark - ASStackLayoutable + +@property (nonatomic, readwrite) CGFloat spacingBefore; +@property (nonatomic, readwrite) CGFloat spacingAfter; +@property (nonatomic, readwrite) BOOL flexGrow; +@property (nonatomic, readwrite) BOOL flexShrink; +@property (nonatomic, readwrite) ASRelativeDimension flexBasis; +@property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf; +@property (nonatomic, readwrite) CGFloat ascender; +@property (nonatomic, readwrite) CGFloat descender; + +#pragma mark - ASStaticLayoutable + +@property (nonatomic, readwrite) ASRelativeSizeRange sizeRange; +@property (nonatomic, readwrite) CGPoint layoutPosition; + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutOptions.mm b/AsyncDisplayKit/Layout/ASLayoutOptions.mm new file mode 100644 index 0000000000..67481cae02 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutOptions.mm @@ -0,0 +1,262 @@ +/* + * Copyright (c) 2014-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 "ASLayoutOptions.h" + +#import +#import +#import +#import "ASInternalHelpers.h" + +@interface ASLayoutOptions() +{ + ASDN::RecursiveMutex _propertyLock; +} +@end + +@implementation ASLayoutOptions + +@synthesize spacingBefore = _spacingBefore; +@synthesize spacingAfter = _spacingAfter; +@synthesize flexGrow = _flexGrow; +@synthesize flexShrink = _flexShrink; +@synthesize flexBasis = _flexBasis; +@synthesize alignSelf = _alignSelf; + +@synthesize ascender = _ascender; +@synthesize descender = _descender; + +@synthesize sizeRange = _sizeRange; +@synthesize layoutPosition = _layoutPosition; + +static Class gDefaultLayoutOptionsClass = nil; ++ (void)setDefaultLayoutOptionsClass:(Class)defaultLayoutOptionsClass +{ + gDefaultLayoutOptionsClass = defaultLayoutOptionsClass; +} + ++ (Class)defaultLayoutOptionsClass +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (gDefaultLayoutOptionsClass == nil) { + // If someone is asking for this and it hasn't been customized yet, use the default. + gDefaultLayoutOptionsClass = [ASLayoutOptions class]; + } + }); + return gDefaultLayoutOptionsClass; +} + +- (instancetype)init +{ + return [self initWithLayoutable:nil]; +} + +- (instancetype)initWithLayoutable:(id)layoutable; +{ + self = [super init]; + if (self) { + + self.flexBasis = ASRelativeDimensionUnconstrained; + self.sizeRange = ASRelativeSizeRangeMake(ASRelativeSizeMakeWithCGSize(CGSizeZero), ASRelativeSizeMakeWithCGSize(CGSizeZero)); + self.layoutPosition = CGPointZero; + + // The following properties use a default value of 0 which we do not need to assign. + // self.spacingBefore = 0; + // self.spacingAfter = 0; + // self.flexGrow = NO; + // self.flexShrink = NO; + // self.alignSelf = ASStackLayoutAlignSelfAuto; + // self.ascender = 0; + // self.descender = 0; + + [self setValuesFromLayoutable:layoutable]; + } + return self; +} + +#pragma mark - NSCopying +- (id)copyWithZone:(NSZone *)zone +{ + ASLayoutOptions *copy = [[[self class] alloc] init]; + [copy copyIntoOptions:self]; + return copy; +} + +- (void)copyIntoOptions:(ASLayoutOptions *)layoutOptions +{ + ASDN::MutexLocker l(_propertyLock); + self.flexBasis = layoutOptions.flexBasis; + self.spacingAfter = layoutOptions.spacingAfter; + self.spacingBefore = layoutOptions.spacingBefore; + self.flexGrow = layoutOptions.flexGrow; + self.flexShrink = layoutOptions.flexShrink; + + self.ascender = layoutOptions.ascender; + self.descender = layoutOptions.descender; + + self.sizeRange = layoutOptions.sizeRange; + self.layoutPosition = layoutOptions.layoutPosition; +} + +/** + * Given an id, set up layout options that are intrinsically defined by the layoutable. + * + * While this could be done in the layoutable object itself, moving the logic into the ASLayoutOptions class + * allows a custom spec to set up defaults without needing to alter the layoutable itself. For example, + * image you were creating a custom baseline spec that needed ascender/descender. To assign values automatically + * when a text node's attribute string is set, you would need to subclass ASTextNode and assign the values in the + * override of setAttributeString. However, assigning the defaults in an ASLayoutOptions subclass's + * setValuesFromLayoutable allows you to create a custom spec without the need to create a + * subclass of ASTextNode. + * + * @param layoutable The layoutable object to inspect for default intrinsic layout option values + */ +- (void)setValuesFromLayoutable:(id)layoutable +{ + ASDN::MutexLocker l(_propertyLock); + if ([layoutable isKindOfClass:[ASDisplayNode class]]) { + ASDisplayNode *displayNode = (ASDisplayNode *)layoutable; + self.sizeRange = ASRelativeSizeRangeMake(ASRelativeSizeMakeWithCGSize(displayNode.preferredFrameSize), ASRelativeSizeMakeWithCGSize(displayNode.preferredFrameSize)); + + if ([layoutable isKindOfClass:[ASTextNode class]]) { + ASTextNode *textNode = (ASTextNode *)layoutable; + NSAttributedString *attributedString = textNode.attributedString; + if (attributedString.length > 0) { + CGFloat screenScale = ASScreenScale(); + self.ascender = round([[attributedString attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL] ascender] * screenScale)/screenScale; + self.descender = round([[attributedString attribute:NSFontAttributeName atIndex:attributedString.length - 1 effectiveRange:NULL] descender] * screenScale)/screenScale; + } + } + + } +} + +- (CGFloat)spacingAfter +{ + ASDN::MutexLocker l(_propertyLock); + return _spacingAfter; +} + +- (void)setSpacingAfter:(CGFloat)spacingAfter +{ + ASDN::MutexLocker l(_propertyLock); + _spacingAfter = spacingAfter; +} + +- (CGFloat)spacingBefore +{ + ASDN::MutexLocker l(_propertyLock); + return _spacingBefore; +} + +- (void)setSpacingBefore:(CGFloat)spacingBefore +{ + ASDN::MutexLocker l(_propertyLock); + _spacingBefore = spacingBefore; +} + +- (BOOL)flexGrow +{ + ASDN::MutexLocker l(_propertyLock); + return _flexGrow; +} + +- (void)setFlexGrow:(BOOL)flexGrow +{ + ASDN::MutexLocker l(_propertyLock); + _flexGrow = flexGrow; +} + +- (BOOL)flexShrink +{ + ASDN::MutexLocker l(_propertyLock); + return _flexShrink; +} + +- (void)setFlexShrink:(BOOL)flexShrink +{ + ASDN::MutexLocker l(_propertyLock); + _flexShrink = flexShrink; +} + +- (ASRelativeDimension)flexBasis +{ + ASDN::MutexLocker l(_propertyLock); + return _flexBasis; +} + +- (void)setFlexBasis:(ASRelativeDimension)flexBasis +{ + ASDN::MutexLocker l(_propertyLock); + _flexBasis = flexBasis; +} + +- (ASStackLayoutAlignSelf)alignSelf +{ + ASDN::MutexLocker l(_propertyLock); + return _alignSelf; +} + +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + ASDN::MutexLocker l(_propertyLock); + _alignSelf = alignSelf; +} + +- (CGFloat)ascender +{ + ASDN::MutexLocker l(_propertyLock); + return _ascender; +} + +- (void)setAscender:(CGFloat)ascender +{ + ASDN::MutexLocker l(_propertyLock); + _ascender = ascender; +} + +- (CGFloat)descender +{ + ASDN::MutexLocker l(_propertyLock); + return _descender; +} + +- (void)setDescender:(CGFloat)descender +{ + ASDN::MutexLocker l(_propertyLock); + _descender = descender; +} + +- (ASRelativeSizeRange)sizeRange +{ + ASDN::MutexLocker l(_propertyLock); + return _sizeRange; +} + +- (void)setSizeRange:(ASRelativeSizeRange)sizeRange +{ + ASDN::MutexLocker l(_propertyLock); + _sizeRange = sizeRange; +} + +- (CGPoint)layoutPosition +{ + ASDN::MutexLocker l(_propertyLock); + return _layoutPosition; +} + +- (void)setLayoutPosition:(CGPoint)layoutPosition +{ + ASDN::MutexLocker l(_propertyLock); + _layoutPosition = layoutPosition; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutOptionsPrivate.mm b/AsyncDisplayKit/Layout/ASLayoutOptionsPrivate.mm new file mode 100644 index 0000000000..64b3f7b629 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutOptionsPrivate.mm @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2014-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 "ASLayoutOptionsPrivate.h" +#import +#import "ASThread.h" + + +/** + * Both an ASDisplayNode and an ASLayoutSpec conform to ASLayoutable. There are several properties + * in ASLayoutable that are used as layoutOptions when a node or spec is used in a layout spec. + * These properties are provided for convenience, as they are forwards to the node or spec's + * ASLayoutOptions class. Instead of duplicating the property forwarding in both classes, we + * create a define that allows us to easily implement the forwards in one place. + * + * If you create a custom layout spec, we recommend this stragety if you decide to extend + * ASDisplayNode and ASLAyoutSpec to provide convenience properties for any options that your + * layoutSpec may require. + */ +#define ASLayoutOptionsForwarding \ +- (ASLayoutOptions *)layoutOptions\ +{\ +ASDN::MutexLocker l(_layoutOptionsLock);\ +if (_layoutOptions == nil) {\ +_layoutOptions = [[[ASLayoutOptions defaultLayoutOptionsClass] alloc] initWithLayoutable:self];\ +}\ +return _layoutOptions;\ +}\ +\ +- (CGFloat)spacingBefore\ +{\ +return self.layoutOptions.spacingBefore;\ +}\ +\ +- (void)setSpacingBefore:(CGFloat)spacingBefore\ +{\ +self.layoutOptions.spacingBefore = spacingBefore;\ +}\ +\ +- (CGFloat)spacingAfter\ +{\ +return self.layoutOptions.spacingAfter;\ +}\ +\ +- (void)setSpacingAfter:(CGFloat)spacingAfter\ +{\ +self.layoutOptions.spacingAfter = spacingAfter;\ +}\ +\ +- (BOOL)flexGrow\ +{\ +return self.layoutOptions.flexGrow;\ +}\ +\ +- (void)setFlexGrow:(BOOL)flexGrow\ +{\ +self.layoutOptions.flexGrow = flexGrow;\ +}\ +\ +- (BOOL)flexShrink\ +{\ +return self.layoutOptions.flexShrink;\ +}\ +\ +- (void)setFlexShrink:(BOOL)flexShrink\ +{\ +self.layoutOptions.flexShrink = flexShrink;\ +}\ +\ +- (ASRelativeDimension)flexBasis\ +{\ +return self.layoutOptions.flexBasis;\ +}\ +\ +- (void)setFlexBasis:(ASRelativeDimension)flexBasis\ +{\ +self.layoutOptions.flexBasis = flexBasis;\ +}\ +\ +- (ASStackLayoutAlignSelf)alignSelf\ +{\ +return self.layoutOptions.alignSelf;\ +}\ +\ +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf\ +{\ + self.layoutOptions.alignSelf = alignSelf;\ +}\ +\ +- (CGFloat)ascender\ +{\ + return self.layoutOptions.ascender;\ +}\ +\ +- (void)setAscender:(CGFloat)ascender\ +{\ + self.layoutOptions.ascender = ascender;\ +}\ +\ +- (CGFloat)descender\ +{\ + return self.layoutOptions.descender;\ +}\ +\ +- (void)setDescender:(CGFloat)descender\ +{\ + self.layoutOptions.descender = descender;\ +}\ +\ +- (ASRelativeSizeRange)sizeRange\ +{\ + return self.layoutOptions.sizeRange;\ +}\ +\ +- (void)setSizeRange:(ASRelativeSizeRange)sizeRange\ +{\ + self.layoutOptions.sizeRange = sizeRange;\ +}\ +\ +- (CGPoint)layoutPosition\ +{\ + return self.layoutOptions.layoutPosition;\ +}\ +\ +- (void)setLayoutPosition:(CGPoint)position\ +{\ + self.layoutOptions.layoutPosition = position;\ +}\ + + +@implementation ASDisplayNode(ASLayoutOptions) +ASLayoutOptionsForwarding +@end + +@implementation ASLayoutSpec(ASLayoutOptions) +ASLayoutOptionsForwarding +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.h b/AsyncDisplayKit/Layout/ASLayoutSpec.h index 122033d091..2dd00198ff 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.h @@ -9,9 +9,90 @@ */ #import -#import /** A layout spec is an immutable object that describes a layout, loosely inspired by React. */ @interface ASLayoutSpec : NSObject +/** + * Creation of a layout spec should only happen by a user in layoutSpecThatFits:. During that method, a + * layout spec can be created and mutated. Once it is passed back to ASDK, the isMutable flag will be + * set to NO and any further mutations will cause an assert. + */ +@property (nonatomic, assign) BOOL isMutable; + +- (instancetype)init; + +/** + * Adds a child to this layout spec using a default identifier. + * + * @param child A child to be added. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * reponsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, + * only require a single child. + * + * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) + * a subclass should use this method to set the "primary" child. It can then use setChild:forIdentifier: + * to set any other required children. Ideally a subclass would hide this from the user, and use the + * setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild + * property that behind the scenes is calling setChild:forIdentifier:. + */ +- (void)setChild:(id)child; + +/** + * Adds a child with the given identifier to this layout spec. + * + * @param child A child to be added. + * + * @param identifier An identifier associated with the child. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * reponsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, + * only require a single child. + * + * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) + * a subclass should use the setChild method to set the "primary" child. It can then use this method + * to set any other required children. Ideally a subclass would hide this from the user, and use the + * setChild:forIdentifier: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild + * property that behind the scenes is calling setChild:forIdentifier:. + */ +- (void)setChild:(id)child forIdentifier:(NSString *)identifier; + +/** + * Adds childen to this layout spec. + * + * @param children An array of ASLayoutable children to be added. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * reponsibility of holding on to the spec children. Some layout specs, like ASStackLayoutSpec, + * can take an unknown number of children. In this case, the this method should be used. + * For good measure, in these layout specs it probably makes sense to define + * setChild: and setChild:forIdentifier: methods to do something appropriate or to assert. + */ +- (void)setChildren:(NSArray *)children; + +/** + * Get child methods + * + * There is a corresponding "getChild" method for the above "setChild" methods. If a subclass + * has extra layoutable children, it is recommended to make a corresponding get method for that + * child. For example, the ASBackgroundLayoutSpec responds to backgroundChild. + * + * If a get method is called on a spec that doesn't make sense, then the standard is to assert. + * For example, calling children on an ASInsetLayoutSpec will assert. + */ + +/** Returns the child added to this layout spec using the default identifier. */ +- (id)child; + +/** + * Returns the child added to this layout spec using the given identifier. + * + * @param identifier An identifier associated withe the child. + */ +- (id)childForIdentifier:(NSString *)identifier; + +/** Returns all children added to this layout spec. */ +- (NSArray *)children; + @end diff --git a/AsyncDisplayKit/Layout/ASLayoutSpec.mm b/AsyncDisplayKit/Layout/ASLayoutSpec.mm index 8b3efd6f4d..46e56dff4a 100644 --- a/AsyncDisplayKit/Layout/ASLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASLayoutSpec.mm @@ -15,30 +15,112 @@ #import "ASInternalHelpers.h" #import "ASLayout.h" +#import "ASLayoutOptions.h" +#import "ASLayoutOptionsPrivate.h" +#import "ASThread.h" + +#import + +static NSString * const kDefaultChildKey = @"kDefaultChildKey"; +static NSString * const kDefaultChildrenKey = @"kDefaultChildrenKey"; + +@interface ASLayoutSpec() +@property (nonatomic, strong) NSMutableDictionary *layoutChildren; +@end @implementation ASLayoutSpec -@synthesize spacingBefore = _spacingBefore; -@synthesize spacingAfter = _spacingAfter; -@synthesize flexGrow = _flexGrow; -@synthesize flexShrink = _flexShrink; -@synthesize flexBasis = _flexBasis; -@synthesize alignSelf = _alignSelf; +// these dynamic properties all defined in ASLayoutOptionsPrivate.m +@dynamic spacingAfter, spacingBefore, flexGrow, flexShrink, flexBasis, alignSelf, ascender, descender, sizeRange, layoutPosition, layoutOptions; +@synthesize layoutChildren = _layoutChildren; +@synthesize isFinalLayoutable = _isFinalLayoutable; -+ (instancetype)new +- (instancetype)init { - ASLayoutSpec *spec = [super new]; - if (spec) { - spec->_flexBasis = ASRelativeDimensionUnconstrained; + if (!(self = [super init])) { + return nil; } - return spec; + _layoutChildren = [NSMutableDictionary dictionary]; + _isMutable = YES; + return self; } #pragma mark - Layout - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - return [ASLayout newWithLayoutableObject:self size:constrainedSize.min]; + return [ASLayout layoutWithLayoutableObject:self size:constrainedSize.min]; } +- (id)finalLayoutable +{ + return self; +} + +- (void)setChild:(id)child; +{ + [self setChild:child forIdentifier:kDefaultChildKey]; +} + +- (id)layoutableToAddFromLayoutable:(id)child +{ + if (self.isFinalLayoutable == NO) { + + // If you are getting recursion crashes here after implementing finalLayoutable, make sure + // that you are setting isFinalLayoutable flag to YES. This must be one BEFORE adding a child + // to the new ASLayoutable. + // + // For example: + //- (id)finalLayoutable + //{ + // ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + // insetSpec.insets = UIEdgeInsetsMake(10,10,10,10); + // insetSpec.isFinalLayoutable = YES; + // [insetSpec setChild:self]; + // return insetSpec; + //} + + id finalLayoutable = [child finalLayoutable]; + if (finalLayoutable != child) { + [finalLayoutable.layoutOptions copyIntoOptions:child.layoutOptions]; + return finalLayoutable; + } + } + return child; +} + +- (void)setChild:(id)child forIdentifier:(NSString *)identifier +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + self.layoutChildren[identifier] = [self layoutableToAddFromLayoutable:child];; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + + NSMutableArray *finalChildren = [NSMutableArray arrayWithCapacity:children.count]; + for (id child in children) { + [finalChildren addObject:[self layoutableToAddFromLayoutable:child]]; + } + + self.layoutChildren[kDefaultChildrenKey] = [NSArray arrayWithArray:finalChildren]; +} + +- (id)childForIdentifier:(NSString *)identifier +{ + return self.layoutChildren[identifier]; +} + +- (id)child +{ + return self.layoutChildren[kDefaultChildKey]; +} + +- (NSArray *)children +{ + return self.layoutChildren[kDefaultChildrenKey]; +} + + @end diff --git a/AsyncDisplayKit/Layout/ASLayoutable.h b/AsyncDisplayKit/Layout/ASLayoutable.h index 5fc091b12f..7e0e950d3f 100644 --- a/AsyncDisplayKit/Layout/ASLayoutable.h +++ b/AsyncDisplayKit/Layout/ASLayoutable.h @@ -9,13 +9,48 @@ */ #import -#import +#import +#import +#import +#import + +#import @class ASLayout; - -@protocol ASLayoutable +@class ASLayoutSpec; /** + * The ASLayoutable protocol declares a method for measuring the layout of an object. A layout + * is defined by an ASLayout return value, and must specify 1) the size (but not position) of the + * layoutable object, and 2) the size and position of all of its immediate child objects. The tree + * recursion is driven by parents requesting layouts from their children in order to determine their + * size, followed by the parents setting the position of the children once the size is known + * + * The protocol also implements a "family" of Layoutable protocols. These protocols contain layout + * options that can be used for specific layout specs. For example, ASStackLayoutSpec has options + * defining how a layoutable should shrink or grow based upon available space. + * + * These layout options are all stored in an ASLayoutOptions class (that is defined in ASLayoutablePrivate). + * Generally you needn't worry about the layout options class, as the layoutable protocols allow all direct + * access to the options via convenience properties. If you are creating custom layout spec, then you can + * extend the backing layout options class to accomodate any new layout options. + */ +@protocol ASLayoutable + +/** + * @abstract Calculate a layout based on given size range. + * + * @param constrainedSize The minimum and maximum sizes the receiver should fit in. + * + * @return An ASLayout instance defining the layout of the receiver and its children. + */ +- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize; + + +#pragma mark - Layout options from the Layoutable Protocols + +#pragma mark - ASStackLayoutable +/** * @abstract Additional space to place before this object in the stacking direction. * Used when attached to a stack layout. */ @@ -53,12 +88,22 @@ @property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf; /** - * @abstract Calculate a layout based on given size range. - * - * @param constrainedSize The minimum and maximum sizes the receiver should fit in. - * - * @return An ASLayout instance defining the layout of the receiver and its children. + * @abstract Used for baseline alignment. The distance from the top of the object to its baseline. */ -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize; +@property (nonatomic, readwrite) CGFloat ascender; + +/** + * @abstract Used for baseline alignment. The distance from the baseline of the object to its bottom. + */ +@property (nonatomic, readwrite) CGFloat descender; + +#pragma mark - ASStaticLayoutable +/** + If specified, the child's size is restricted according to this size. Percentages are resolved relative to the static layout spec. + */ +@property (nonatomic, assign) ASRelativeSizeRange sizeRange; + +/** The position of this object within its parent spec. */ +@property (nonatomic, assign) CGPoint layoutPosition; @end diff --git a/AsyncDisplayKit/Layout/ASLayoutablePrivate.h b/AsyncDisplayKit/Layout/ASLayoutablePrivate.h new file mode 100644 index 0000000000..f52dd54ad6 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutablePrivate.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2014-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 ASLayoutSpec; +@class ASLayoutOptions; +@protocol ASLayoutable; + +/** + * The base protocol for ASLayoutable. Generally the methods/properties in this class do not need to be + * called by the end user and are only called internally. However, there may be a case where the methods are useful. + */ +@protocol ASLayoutablePrivate + +/** + * @abstract This method can be used to give the user a chance to wrap an ASLayoutable in an ASLayoutSpec + * just before it is added to a parent ASLayoutSpec. For example, if you wanted an ASTextNode that was always + * inside of an ASInsetLayoutSpec, you could subclass ASTextNode and implement finalLayoutable so that it wraps + * itself in an inset spec. + * + * Note that any ASLayoutable other than self that is returned MUST set isFinalLayoutable to YES. Make sure + * to do this BEFORE adding a child to the ASLayoutable. + * + * @return The layoutable that will be added to the parent layout spec. Defaults to self. + */ +- (id)finalLayoutable; + +/** + * A flag to indicate that this ASLayoutable was created in finalLayoutable. This MUST be set to YES + * before adding a child to this layoutable. + */ +@property (nonatomic, assign) BOOL isFinalLayoutable; + + +/** + * The class that holds all of the layoutOptions set on an ASLayoutable. + */ +@property (nonatomic, strong, readonly) ASLayoutOptions *layoutOptions; +@end diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.h b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.h index 846b0cece5..05e53d92e8 100644 --- a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.h @@ -15,6 +15,8 @@ */ @interface ASOverlayLayoutSpec : ASLayoutSpec -+ (instancetype)newWithChild:(id)child overlay:(id)overlay; +@property (nonatomic, strong) id overlay; + ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay; @end diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm index 074753f143..1fdfd5fc28 100644 --- a/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutSpec.mm @@ -12,27 +12,36 @@ #import "ASAssert.h" #import "ASBaseDefines.h" +#import "ASLayout.h" + +static NSString * const kOverlayChildKey = @"kOverlayChildKey"; @implementation ASOverlayLayoutSpec -{ - id _overlay; - id _child; -} -+ (instancetype)newWithChild:(id)child overlay:(id)overlay +- (instancetype)initWithChild:(id)child overlay:(id)overlay { - ASOverlayLayoutSpec *spec = [super new]; - if (spec) { - ASDisplayNodeAssertNotNil(child, @"Child that will be overlayed on shouldn't be nil"); - spec->_overlay = overlay; - spec->_child = child; + if (!(self = [super init])) { + return nil; } - return spec; + ASDisplayNodeAssertNotNil(child, @"Child that will be overlayed on shouldn't be nil"); + self.overlay = overlay; + [self setChild:child]; + return self; } -+ (instancetype)new ++ (instancetype)overlayLayoutSpecWithChild:(id)child overlay:(id)overlay { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + return [[self alloc] initWithChild:child overlay:overlay]; +} + +- (void)setOverlay:(id)overlay +{ + [super setChild:overlay forIdentifier:kOverlayChildKey]; +} + +- (id)overlay +{ + return [super childForIdentifier:kOverlayChildKey]; } /** @@ -40,16 +49,27 @@ */ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - ASLayout *contentsLayout = [_child measureWithSizeRange:constrainedSize]; + ASLayout *contentsLayout = [self.child measureWithSizeRange:constrainedSize]; contentsLayout.position = CGPointZero; NSMutableArray *sublayouts = [NSMutableArray arrayWithObject:contentsLayout]; - if (_overlay) { - ASLayout *overlayLayout = [_overlay measureWithSizeRange:{contentsLayout.size, contentsLayout.size}]; + if (self.overlay) { + ASLayout *overlayLayout = [self.overlay measureWithSizeRange:{contentsLayout.size, contentsLayout.size}]; overlayLayout.position = CGPointZero; [sublayouts addObject:overlayLayout]; } - return [ASLayout newWithLayoutableObject:self size:contentsLayout.size sublayouts:sublayouts]; + return [ASLayout layoutWithLayoutableObject:self size:contentsLayout.size sublayouts:sublayouts]; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); +} + +- (NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.h b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.h index 73be620d71..2affa56a75 100644 --- a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.h @@ -31,6 +31,8 @@ **/ @interface ASRatioLayoutSpec : ASLayoutSpec -+ (instancetype)newWithRatio:(CGFloat)ratio child:(id)child; +@property (nonatomic, assign) CGFloat ratio; + ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child; @end diff --git a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm index 103be815a7..eac7464aa3 100644 --- a/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASRatioLayoutSpec.mm @@ -17,31 +17,34 @@ #import "ASBaseDefines.h" #import "ASInternalHelpers.h" +#import "ASLayout.h" @implementation ASRatioLayoutSpec { CGFloat _ratio; - id _child; } -+ (instancetype)newWithRatio:(CGFloat)ratio child:(id)child ++ (instancetype)ratioLayoutSpecWithRatio:(CGFloat)ratio child:(id)child { - ASDisplayNodeAssert(ratio > 0, @"Ratio should be strictly positive, but received %f", ratio); - if (child == nil) { + return [[self alloc] initWithRatio:ratio child:child]; +} + +- (instancetype)initWithRatio:(CGFloat)ratio child:(id)child; +{ + if (!(self = [super init])) { return nil; } - - ASRatioLayoutSpec *spec = [super new]; - if (spec) { - spec->_ratio = ratio; - spec->_child = child; - } - return spec; + ASDisplayNodeAssertNotNil(child, @"Child cannot be nil"); + ASDisplayNodeAssert(ratio > 0, @"Ratio should be strictly positive, but received %f", ratio); + _ratio = ratio; + [self setChild:child]; + return self; } -+ (instancetype)new +- (void)setRatio:(CGFloat)ratio { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _ratio = ratio; } - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize @@ -67,9 +70,20 @@ // If there is no max size in *either* dimension, we can't apply the ratio, so just pass our size range through. const ASSizeRange childRange = (bestSize == sizeOptions.end()) ? constrainedSize : ASSizeRangeMake(*bestSize, *bestSize); - ASLayout *sublayout = [_child measureWithSizeRange:childRange]; + ASLayout *sublayout = [self.child measureWithSizeRange:childRange]; sublayout.position = CGPointZero; - return [ASLayout newWithLayoutableObject:self size:sublayout.size sublayouts:@[sublayout]]; + return [ASLayout layoutWithLayoutableObject:self size:sublayout.size sublayouts:@[sublayout]]; +} + +- (void)setChildren:(NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); +} + +- (NSArray *)children +{ + ASDisplayNodeAssert(NO, @"not supported by this layout spec"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.h b/AsyncDisplayKit/Layout/ASRelativeSize.h similarity index 100% rename from AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.h rename to AsyncDisplayKit/Layout/ASRelativeSize.h diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.mm b/AsyncDisplayKit/Layout/ASRelativeSize.mm similarity index 98% rename from AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.mm rename to AsyncDisplayKit/Layout/ASRelativeSize.mm index 4790cd1582..0410bb5a3c 100644 --- a/AsyncDisplayKit/Layout/ASStaticLayoutSpecDimension.mm +++ b/AsyncDisplayKit/Layout/ASRelativeSize.mm @@ -8,7 +8,7 @@ * */ -#import "ASStaticLayoutSpecDimension.h" +#import "ASRelativeSize.h" #import "ASAssert.h" ASRelativeSizeRange const ASRelativeSizeRangeUnconstrained = {}; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutChild.h b/AsyncDisplayKit/Layout/ASStackLayoutChild.h deleted file mode 100644 index 040d8ac01a..0000000000 --- a/AsyncDisplayKit/Layout/ASStackLayoutChild.h +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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. - * - */ - -/** - Each child may override their parent stack's cross axis alignment. - @see ASStackLayoutAlignItems - */ -typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { - /** Inherit alignment value from containing stack. */ - ASStackLayoutAlignSelfAuto, - ASStackLayoutAlignSelfStart, - ASStackLayoutAlignSelfEnd, - ASStackLayoutAlignSelfCenter, - ASStackLayoutAlignSelfStretch, -}; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h new file mode 100644 index 0000000000..82d1aa0daf --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -0,0 +1,73 @@ +/* + * 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. + * + */ + +/** The direction children are stacked in */ +typedef NS_ENUM(NSUInteger, ASStackLayoutDirection) { + /** Children are stacked vertically */ + ASStackLayoutDirectionVertical, + /** Children are stacked horizontally */ + ASStackLayoutDirectionHorizontal, +}; + +/** If no children are flexible, how should this spec justify its children in the available space? */ +typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { + /** + On overflow, children overflow out of this spec's bounds on the right/bottom side. + On underflow, children are left/top-aligned within this spec's bounds. + */ + ASStackLayoutJustifyContentStart, + /** + On overflow, children are centered and overflow on both sides. + On underflow, children are centered within this spec's bounds in the stacking direction. + */ + ASStackLayoutJustifyContentCenter, + /** + On overflow, children overflow out of this spec's bounds on the left/top side. + On underflow, children are right/bottom-aligned within this spec's bounds. + */ + ASStackLayoutJustifyContentEnd, +}; + +/** Orientation of children along cross axis */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems) { + /** Align children to start of cross axis */ + ASStackLayoutAlignItemsStart, + /** Align children with end of cross axis */ + ASStackLayoutAlignItemsEnd, + /** Center children on cross axis */ + ASStackLayoutAlignItemsCenter, + /** Expand children to fill cross axis */ + ASStackLayoutAlignItemsStretch, + /** Children align to their first baseline. Only available for horizontal stack nodes */ + ASStackLayoutAlignItemsBaselineFirst, + /** Children align to their last baseline. Only available for horizontal stack nodes */ + ASStackLayoutAlignItemsBaselineLast, +}; + +/** + Each child may override their parent stack's cross axis alignment. + @see ASStackLayoutAlignItems + */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { + /** Inherit alignment value from containing stack. */ + ASStackLayoutAlignSelfAuto, + /** Align to start of cross axis */ + ASStackLayoutAlignSelfStart, + /** Align with end of cross axis */ + ASStackLayoutAlignSelfEnd, + /** Center on cross axis */ + ASStackLayoutAlignSelfCenter, + /** Expand to fill cross axis */ + ASStackLayoutAlignSelfStretch, + /** Children align to their first baseline. Only available for horizontal stack nodes */ + ASStackLayoutAlignSelfBaselineFirst, + /** Children align to their last baseline. Only available for horizontal stack nodes */ + ASStackLayoutAlignSelfBaselineLast, +}; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h index 83322b8de5..dd331f1405 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h @@ -9,52 +9,8 @@ */ #import +#import -typedef NS_ENUM(NSUInteger, ASStackLayoutDirection) { - ASStackLayoutDirectionVertical, - ASStackLayoutDirectionHorizontal, -}; - -/** If no children are flexible, how should this spec justify its children in the available space? */ -typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { - /** - On overflow, children overflow out of this spec's bounds on the right/bottom side. - On underflow, children are left/top-aligned within this spec's bounds. - */ - ASStackLayoutJustifyContentStart, - /** - On overflow, children are centered and overflow on both sides. - On underflow, children are centered within this spec's bounds in the stacking direction. - */ - ASStackLayoutJustifyContentCenter, - /** - On overflow, children overflow out of this spec's bounds on the left/top side. - On underflow, children are right/bottom-aligned within this spec's bounds. - */ - ASStackLayoutJustifyContentEnd, -}; - -typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems) { - /** Align children to start of cross axis */ - ASStackLayoutAlignItemsStart, - /** Align children with end of cross axis */ - ASStackLayoutAlignItemsEnd, - /** Center children on cross axis */ - ASStackLayoutAlignItemsCenter, - /** Expand children to fill cross axis */ - ASStackLayoutAlignItemsStretch, -}; - -typedef struct { - /** Specifies the direction children are stacked in. */ - ASStackLayoutDirection direction; - /** The amount of space between each child. */ - CGFloat spacing; - /** How children are aligned if there are no flexible children. */ - ASStackLayoutJustifyContent justifyContent; - /** Orientation of children along cross axis */ - ASStackLayoutAlignItems alignItems; -} ASStackLayoutSpecStyle; /** A simple layout spec that stacks a list of children vertically or horizontally. @@ -68,6 +24,7 @@ typedef struct { justifyContent determines how children are laid out. For example: + - Suppose stacking direction is Vertical, min-width=100, max-width=300, min-height=200, max-height=500. - All children are laid out with min-width=100, max-width=300, min-height=0, max-height=INFINITY. - If the sum of the childrens' heights is less than 200, children with flexGrow are flexed larger. @@ -78,10 +35,26 @@ typedef struct { */ @interface ASStackLayoutSpec : ASLayoutSpec +/** Specifies the direction children are stacked in. */ +@property (nonatomic, assign) ASStackLayoutDirection direction; +/** The amount of space between each child. */ +@property (nonatomic, assign) CGFloat spacing; +/** The amount of space between each child. */ +@property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; +/** Orientation of children along cross axis */ +@property (nonatomic, assign) ASStackLayoutAlignItems alignItems; +/** If YES the vertical spacing between two views is measured from the last baseline of the top view to the top of the bottom view */ +@property (nonatomic, assign) BOOL baselineRelativeArrangement; + +- (instancetype)init; + /** - @param style Specifies how children are laid out. + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis @param children ASLayoutable children to be positioned. */ -+ (instancetype)newWithStyle:(ASStackLayoutSpecStyle)style children:(NSArray *)children; ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children; @end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 75b699c55b..3f0abf42fa 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -17,43 +17,114 @@ #import "ASInternalHelpers.h" #import "ASLayoutSpecUtilities.h" +#import "ASStackBaselinePositionedLayout.h" #import "ASStackLayoutSpecUtilities.h" #import "ASStackPositionedLayout.h" #import "ASStackUnpositionedLayout.h" +#import "ASThread.h" @implementation ASStackLayoutSpec { - ASStackLayoutSpecStyle _style; - std::vector> _children; + ASDN::RecursiveMutex _propertyLock; } -+ (instancetype)newWithStyle:(ASStackLayoutSpecStyle)style children:(NSArray *)children +- (instancetype)init { - ASStackLayoutSpec *spec = [super new]; - if (spec) { - spec->_style = style; - spec->_children = std::vector>(); - for (id child in children) { - spec->_children.push_back(child); - } + return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStart children:nil]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems children:children]; +} + +- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children +{ + if (!(self = [super init])) { + return nil; } - return spec; + _direction = direction; + _alignItems = alignItems; + _spacing = spacing; + _justifyContent = justifyContent; + + [self setChildren:children]; + return self; } -+ (instancetype)new +- (void)setDirection:(ASStackLayoutDirection)direction { - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _direction = direction; +} + +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _alignItems = alignItems; +} + +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _justifyContent = justifyContent; +} + +- (void)setSpacing:(CGFloat)spacing +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _spacing = spacing; +} + +- (void)setBaselineRelativeArrangement:(BOOL)baselineRelativeArrangement +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + _baselineRelativeArrangement = baselineRelativeArrangement; +} + +- (void)setChild:(id)child forIdentifier:(NSString *)identifier +{ + ASDisplayNodeAssert(NO, @"ASStackLayoutSpec only supports setChildren"); +} + +- (id)childForIdentifier:(NSString *)identifier +{ + ASDisplayNodeAssert(NO, @"ASStackLayoutSpec only supports children"); + return nil; } - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { - const auto unpositionedLayout = ASStackUnpositionedLayout::compute(_children, _style, constrainedSize); - const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, _style, constrainedSize); - const CGSize finalSize = directionSize(_style.direction, unpositionedLayout.stackDimensionSum, positionedLayout.crossSize); - NSArray *sublayouts = [NSArray arrayWithObjects:&positionedLayout.sublayouts[0] count:positionedLayout.sublayouts.size()]; - return [ASLayout newWithLayoutableObject:self - size:ASSizeRangeClamp(constrainedSize, finalSize) - sublayouts:sublayouts]; + ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .baselineRelativeArrangement = _baselineRelativeArrangement}; + BOOL needsBaselinePass = _baselineRelativeArrangement || _alignItems == ASStackLayoutAlignItemsBaselineFirst || _alignItems == ASStackLayoutAlignItemsBaselineLast; + + std::vector> stackChildren = std::vector>(); + for (id child in self.children) { + stackChildren.push_back(child); + needsBaselinePass |= child.alignSelf == ASStackLayoutAlignSelfBaselineFirst || child.alignSelf == ASStackLayoutAlignSelfBaselineLast; + } + + const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize); + const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, style, constrainedSize); + + CGSize finalSize = CGSizeZero; + NSArray *sublayouts = nil; + if (needsBaselinePass) { + const auto baselinePositionedLayout = ASStackBaselinePositionedLayout::compute(positionedLayout, style, constrainedSize); + ASDN::MutexLocker l(_propertyLock); + self.ascender = baselinePositionedLayout.ascender; + self.descender = baselinePositionedLayout.descender; + + finalSize = directionSize(style.direction, unpositionedLayout.stackDimensionSum, baselinePositionedLayout.crossSize); + sublayouts = [NSArray arrayWithObjects:&baselinePositionedLayout.sublayouts[0] count:baselinePositionedLayout.sublayouts.size()]; + } else { + finalSize = directionSize(style.direction, unpositionedLayout.stackDimensionSum, positionedLayout.crossSize); + sublayouts = [NSArray arrayWithObjects:&positionedLayout.sublayouts[0] count:positionedLayout.sublayouts.size()]; + } + + return [ASLayout layoutWithLayoutableObject:self + size:ASSizeRangeClamp(constrainedSize, finalSize) + sublayouts:sublayouts]; } @end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutable.h b/AsyncDisplayKit/Layout/ASStackLayoutable.h new file mode 100644 index 0000000000..3ebc9304f0 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStackLayoutable.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014-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 +#import + +/** + * Layout options that can be defined for an ASLayoutable being added to a ASStackLayoutSpec. + */ +@protocol ASStackLayoutable + +/** + * @abstract Additional space to place before this object in the stacking direction. + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) CGFloat spacingBefore; + +/** + * @abstract Additional space to place after this object in the stacking direction. + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) CGFloat spacingAfter; + +/** + * @abstract If the sum of childrens' stack dimensions is less than the minimum size, should this object grow? + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) BOOL flexGrow; + +/** + * @abstract If the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink? + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) BOOL flexShrink; + +/** + * @abstract Specifies the initial size in the stack dimension for this object. + * Default to ASRelativeDimensionUnconstrained. + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) ASRelativeDimension flexBasis; + +/** + * @abstract Orientation of the object along cross axis, overriding alignItems + * Used when attached to a stack layout. + */ +@property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf; + +/** + * @abstract Used for baseline alignment. The distance from the top of the object to its baseline. + */ +@property (nonatomic, readwrite) CGFloat ascender; + +/** + * @abstract Used for baseline alignment. The distance from the baseline of the object to its bottom. + */ +@property (nonatomic, readwrite) CGFloat descender; + +@end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.h b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.h index 91497c2da2..8bd3f3d9f2 100644 --- a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.h @@ -9,38 +9,18 @@ */ #import -#import - -@interface ASStaticLayoutSpecChild : NSObject - -@property (nonatomic, readonly) CGPoint position; -@property (nonatomic, readonly) id layoutableObject; +#import /** - If specified, the child's size is restricted according to this size. Percentages are resolved relative to the static layout spec. - */ -@property (nonatomic, readonly) ASRelativeSizeRange size; - -+ (instancetype)newWithPosition:(CGPoint)position layoutableObject:(id)layoutableObject size:(ASRelativeSizeRange)size; - -/** - Convenience with default size is Unconstrained in both dimensions, which sets the child's min size to zero - and max size to the maximum available space it can consume without overflowing the spec's bounds. - */ -+ (instancetype)newWithPosition:(CGPoint)position layoutableObject:(id)layoutableObject; - -@end - -/* - A layout spec that positions children at fixed positions. - - Computes a size that is the union of all childrens' frames. + * A layout spec that positions children at fixed positions. + * + * Computes a size that is the union of all childrens' frames. */ @interface ASStaticLayoutSpec : ASLayoutSpec /** - @param children Children to be positioned at fixed positions, each is of type ASStaticLayoutSpecChild. + @param children Children to be positioned at fixed positions, each conforms to ASStaticLayoutable */ -+ (instancetype)newWithChildren:(NSArray *)children; ++ (instancetype)staticLayoutSpecWithChildren:(NSArray *)children; @end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm index f7488a7133..8e8435c9cc 100644 --- a/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStaticLayoutSpec.mm @@ -11,45 +11,30 @@ #import "ASStaticLayoutSpec.h" #import "ASLayoutSpecUtilities.h" +#import "ASLayoutOptions.h" #import "ASInternalHelpers.h" - -@implementation ASStaticLayoutSpecChild - -+ (instancetype)newWithPosition:(CGPoint)position layoutableObject:(id)layoutableObject size:(ASRelativeSizeRange)size -{ - ASStaticLayoutSpecChild *c = [super new]; - if (c) { - c->_position = position; - c->_layoutableObject = layoutableObject; - c->_size = size; - } - return c; -} - -+ (instancetype)newWithPosition:(CGPoint)position layoutableObject:(id)layoutableObject -{ - return [self newWithPosition:position layoutableObject:layoutableObject size:ASRelativeSizeRangeUnconstrained]; -} - -@end +#import "ASLayout.h" +#import "ASStaticLayoutable.h" @implementation ASStaticLayoutSpec + ++ (instancetype)staticLayoutSpecWithChildren:(NSArray *)children { - NSArray *_children; + return [[self alloc] initWithChildren:children]; } -+ (instancetype)newWithChildren:(NSArray *)children +- (instancetype)init { - ASStaticLayoutSpec *spec = [super new]; - if (spec) { - spec->_children = children; + return [self initWithChildren:@[]]; +} + +- (instancetype)initWithChildren:(NSArray *)children +{ + if (!(self = [super init])) { + return nil; } - return spec; -} - -+ (instancetype)new -{ - ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); + self.children = children; + return self; } - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize @@ -59,37 +44,48 @@ constrainedSize.max.height }; - NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:_children.count]; - for (ASStaticLayoutSpecChild *child in _children) { + NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:self.children.count]; + for (id child in self.children) { CGSize autoMaxSize = { - constrainedSize.max.width - child.position.x, - constrainedSize.max.height - child.position.y + constrainedSize.max.width - child.layoutPosition.x, + constrainedSize.max.height - child.layoutPosition.y }; - ASSizeRange childConstraint = ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRangeUnconstrained, child.size) + ASSizeRange childConstraint = ASRelativeSizeRangeEqualToRelativeSizeRange(ASRelativeSizeRangeUnconstrained, child.sizeRange) ? ASSizeRangeMake({0, 0}, autoMaxSize) - : ASRelativeSizeRangeResolve(child.size, size); - ASLayout *sublayout = [child.layoutableObject measureWithSizeRange:childConstraint]; - sublayout.position = child.position; + : ASRelativeSizeRangeResolve(child.sizeRange, size); + ASLayout *sublayout = [child measureWithSizeRange:childConstraint]; + sublayout.position = child.layoutPosition; [sublayouts addObject:sublayout]; } - if (isnan(size.width)) { + if (isnan(size.width) || size.width >= FLT_MAX - FLT_EPSILON) { size.width = constrainedSize.min.width; for (ASLayout *sublayout in sublayouts) { size.width = MAX(size.width, sublayout.position.x + sublayout.size.width); } } - if (isnan(size.height)) { + if (isnan(size.height) || size.height >= FLT_MAX - FLT_EPSILON) { size.height = constrainedSize.min.height; for (ASLayout *sublayout in sublayouts) { size.height = MAX(size.height, sublayout.position.y + sublayout.size.height); } } - return [ASLayout newWithLayoutableObject:self - size:ASSizeRangeClamp(constrainedSize, size) - sublayouts:sublayouts]; + return [ASLayout layoutWithLayoutableObject:self + size:ASSizeRangeClamp(constrainedSize, size) + sublayouts:sublayouts]; +} + +- (void)setChild:(id)child forIdentifier:(NSString *)identifier +{ + ASDisplayNodeAssert(NO, @"ASStackLayoutSpec only supports setChildren"); +} + +- (id)childForIdentifier:(NSString *)identifier +{ + ASDisplayNodeAssert(NO, @"ASStackLayoutSpec only supports children"); + return nil; } @end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutable.h b/AsyncDisplayKit/Layout/ASStaticLayoutable.h new file mode 100644 index 0000000000..e0325cfc05 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStaticLayoutable.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014-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 + +/** + * Layout options that can be defined for an ASLayoutable being added to a ASStaticLayoutSpec. + */ +@protocol ASStaticLayoutable + +/** + If specified, the child's size is restricted according to this size. Percentages are resolved relative to the static layout spec. + */ +@property (nonatomic, assign) ASRelativeSizeRange sizeRange; + +/** The position of this object within its parent spec. */ +@property (nonatomic, assign) CGPoint layoutPosition; + +@end diff --git a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm index 99167a93d4..e6551d7a26 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+AsyncDisplay.mm @@ -83,6 +83,13 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, if (self.isHidden || self.alpha <= 0.0) { return; } + + BOOL rasterizingFromAscendent = [self __rasterizedContainerNode] != nil; + + // if super node is rasterizing descendents, subnodes will not have had layout calls becase they don't have layers + if (rasterizingFromAscendent) { + [self __layout]; + } // Capture these outside the display block so they are retained. UIColor *backgroundColor = self.backgroundColor; @@ -121,6 +128,11 @@ static void __ASDisplayLayerDecrementConcurrentDisplayCount(BOOL displayIsAsync, CGContextTranslateCTM(context, frame.origin.x, frame.origin.y); + //support cornerRadius + if (rasterizingFromAscendent && self.cornerRadius && self.clipsToBounds) { + [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:self.cornerRadius] addClip]; + } + // Fill background if any. CGColorRef backgroundCGColor = backgroundColor.CGColor; if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) { diff --git a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm index 2f2994fd98..ef013a97c2 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm +++ b/AsyncDisplayKit/Private/ASDisplayNode+UIViewBridge.mm @@ -174,6 +174,7 @@ - (void)setNeedsLayout { _bridge_prologue; + [self __setNeedsLayout]; _messageToViewOrLayer(setNeedsLayout); } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index cded26c2c6..7066159c68 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -17,10 +17,11 @@ #import "ASDisplayNode.h" #import "ASSentinel.h" #import "ASThread.h" -#import "ASLayout.h" +#import "ASLayoutOptions.h" BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); void ASDisplayNodePerformBlockOnMainThread(void (^block)()); +void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)()); typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { ASDisplayNodeMethodOverrideNone = 0, @@ -28,7 +29,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, - ASDisplayNodeMethodOverrideCalculateSizeThatFits = 1 << 4 + ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4 }; @class _ASPendingState; @@ -59,6 +60,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; + ASDisplayNodeDidLoadBlock _nodeLoadedBlock; Class _viewClass; Class _layerClass; UIView *_view; @@ -72,7 +74,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { _ASPendingState *_pendingViewState; - struct { + struct ASDisplayNodeFlags { // public properties unsigned synchronous:1; unsigned layerBacked:1; @@ -122,6 +124,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) { // Core implementation of -measureWithSizeRange:. Must be called with _propertyLock held. - (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize; +- (void)__setNeedsLayout; - (void)__layout; - (void)__setSupernode:(ASDisplayNode *)supernode; diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 4349eb011b..6a9d74e97e 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -8,14 +8,14 @@ * */ -#import +#include +#import #import "ASBaseDefines.h" -@class ASLayoutChild; - ASDISPLAYNODE_EXTERN_C_BEGIN BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); +BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector); CGFloat ASScreenScale(); diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm index d161523f2b..f57450d39e 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.mm +++ b/AsyncDisplayKit/Private/ASInternalHelpers.mm @@ -24,6 +24,15 @@ BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector) return (superclassIMP != subclassIMP); } +BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector) +{ + Method superclassMethod = class_getClassMethod(superclass, selector); + Method subclassMethod = class_getClassMethod(subclass, selector); + IMP superclassIMP = superclassMethod ? method_getImplementation(superclassMethod) : NULL; + IMP subclassIMP = subclassMethod ? method_getImplementation(subclassMethod) : NULL; + return (superclassIMP != subclassIMP); +} + static void ASDispatchOnceOnMainThread(dispatch_once_t *predicate, dispatch_block_t block) { if ([NSThread isMainThread]) { diff --git a/AsyncDisplayKit/Private/ASLayoutOptionsPrivate.h b/AsyncDisplayKit/Private/ASLayoutOptionsPrivate.h new file mode 100644 index 0000000000..34a26da4f2 --- /dev/null +++ b/AsyncDisplayKit/Private/ASLayoutOptionsPrivate.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2014-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 +#import +#import + +@interface ASDisplayNode(ASLayoutOptions) +@end + +@interface ASDisplayNode() +{ + ASLayoutOptions *_layoutOptions; + ASDN::RecursiveMutex _layoutOptionsLock; +} +@end + +@interface ASLayoutSpec(ASLayoutOptions) +@end + +@interface ASLayoutSpec() +{ + ASLayoutOptions *_layoutOptions; + ASDN::RecursiveMutex _layoutOptionsLock; +} +@end diff --git a/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.h b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.h new file mode 100644 index 0000000000..6f208e1b29 --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2014-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 "ASLayout.h" +#import "ASDimension.h" +#import "ASStackPositionedLayout.h" + +struct ASStackBaselinePositionedLayout { + const std::vector sublayouts; + const CGFloat crossSize; + const CGFloat ascender; + const CGFloat descender; + + /** Given a positioned layout, computes each child position using baseline alignment. */ + static ASStackBaselinePositionedLayout compute(const ASStackPositionedLayout &positionedLayout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &constrainedSize); +}; diff --git a/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm new file mode 100644 index 0000000000..4e81dd60b5 --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackBaselinePositionedLayout.mm @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2014-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 "ASStackBaselinePositionedLayout.h" + +#import "ASLayoutSpecUtilities.h" +#import "ASStackLayoutSpecUtilities.h" +#import "ASLayoutOptions.h" + +static CGFloat baselineForItem(const ASStackLayoutSpecStyle &style, + const ASLayout *layout) { + + __weak id child = layout.layoutableObject; + switch (style.alignItems) { + case ASStackLayoutAlignItemsBaselineFirst: + return child.ascender; + case ASStackLayoutAlignItemsBaselineLast: + return layout.size.height + child.descender; + default: + return 0; + } + +} + +static CGFloat baselineOffset(const ASStackLayoutSpecStyle &style, + const ASLayout *l, + const CGFloat maxAscender, + const CGFloat maxBaseline) +{ + if (style.direction == ASStackLayoutDirectionHorizontal) { + __weak id child = l.layoutableObject; + switch (style.alignItems) { + case ASStackLayoutAlignItemsBaselineFirst: + return maxAscender - child.ascender; + case ASStackLayoutAlignItemsBaselineLast: + return maxBaseline - baselineForItem(style, l); + + default: + return 0; + } + } + return 0; +} + +static CGFloat maxDimensionForLayout(const ASLayout *l, + const ASStackLayoutSpecStyle &style) +{ + CGFloat maxDimension = crossDimension(style.direction, l.size); + style.direction == ASStackLayoutDirectionVertical ? maxDimension += l.position.x : maxDimension += l.position.y; + return maxDimension; +} + +ASStackBaselinePositionedLayout ASStackBaselinePositionedLayout::compute(const ASStackPositionedLayout &positionedLayout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &constrainedSize) +{ + /* Step 1: Look at each child and determine the distance from the top of the child node it's baseline. + For example, let's say we have the following two text nodes and want to align them to the first baseline: + + Hello! Why, hello there! How + are you today? + + The first node has a font of size 14, the second a font of size 12. The first node will have a baseline offset of + the ascender of a font of size 14, the second will have a baseline of the ascender of a font of size 12. The first + baseline will be larger so we will keep that as the max baseline. + + However, if were to align from the last baseline we'd find the max baseline by taking the height of node and adding + the font's descender (its negative). In the case of the first node, which is only 1 line, this should be the same value as the ascender. + The second node, however, has a larger height and there will have a larger baseline offset. + */ + const auto baselineIt = std::max_element(positionedLayout.sublayouts.begin(), positionedLayout.sublayouts.end(), [&](const ASLayout *a, const ASLayout *b){ + return baselineForItem(style, a) < baselineForItem(style, b); + }); + const CGFloat maxBaseline = baselineIt == positionedLayout.sublayouts.end() ? 0 : baselineForItem(style, *baselineIt); + + /* + Step 2: Find the max ascender for all of the children. + Imagine 3 nodes aligned horizontally, all with the same text but with font sizes of 12, 14, 16. Because it is has the largest + ascender node with font size of 16 will not need to move, the other two nodes will align to this node's baseline. The offset we will use + for each node is our computed maxAscender - node.ascender. If the 16pt node had an ascender of 10 and the 14pt node + had an ascender of 8, that means we will offset the 14pt node by 2 pts. + + Note: if we are alinging to the last baseline, then we don't need this value in our computation. However, we do want + our layoutSpec to have it so that it can be baseline aligned with another text node or baseline layout spec. + */ + const auto ascenderIt = std::max_element(positionedLayout.sublayouts.begin(), positionedLayout.sublayouts.end(), [&](const ASLayout *a, const ASLayout *b){ + return a.layoutableObject.ascender < b.layoutableObject.ascender; + }); + const CGFloat maxAscender = baselineIt == positionedLayout.sublayouts.end() ? 0 : (*ascenderIt).layoutableObject.ascender; + + /* + Step 3: Take each child and update its layout position based on the baseline offset. + + If this is a horizontal stack, we take a positioned child and add to its y offset to align it to the maxBaseline of the children. + If this is a vertical stack, we add the child's descender to the location of the next child to position. This will ensure the + spacing between the two nodes is from the baseline, not the bounding box. + + */ + CGPoint p = CGPointZero; + BOOL first = YES; + auto stackedChildren = AS::map(positionedLayout.sublayouts, [&](ASLayout *l) -> ASLayout *{ + __weak id child = l.layoutableObject; + p = p + directionPoint(style.direction, child.spacingBefore, 0); + if (first) { + // if this is the first item use the previously computed start point + p = l.position; + } else { + // otherwise add the stack spacing + p = p + directionPoint(style.direction, style.spacing, 0); + } + first = NO; + + // Find the difference between this node's baseline and the max baseline of all the children. Add this difference to the child's y position. + l.position = p + CGPointMake(0, baselineOffset(style, l, maxAscender, maxBaseline)); + + // If we are a vertical stack, add the item's descender (it is negative) to the offset for the next node. This will ensure we are spacing + // node from baselines and not bounding boxes. + CGFloat spacingAfterBaseline = 0; + if (style.direction == ASStackLayoutDirectionVertical) { + spacingAfterBaseline = child.descender; + } + p = p + directionPoint(style.direction, stackDimension(style.direction, l.size) + child.spacingAfter + spacingAfterBaseline, 0); + + return l; + }); + + /* + Step 4: Since we have been mucking with positions, there is a chance that our cross size has changed. Imagine a node with a font size of 40 + and another node with a font size of 12 but with multiple lines. We align these nodes to the first baseline, which will be the baseline of the node with + font size of 40 (max ascender). Now, we have to move the node with multiple lines down to the other node's baseline. This node with multiple lines will + extend below the first node farther than it did before aligning the baselines thus increasing the cross size. + + After finding the new cross size, we need to clamp it so that it fits within the constrainted size. + + */ + const auto it = std::max_element(stackedChildren.begin(), stackedChildren.end(), + [&](ASLayout *a, ASLayout *b) { + return maxDimensionForLayout(a, style) < maxDimensionForLayout(b, style); + }); + const auto largestChildCrossSize = it == stackedChildren.end() ? 0 : maxDimensionForLayout(*it, style); + const auto minCrossSize = crossDimension(style.direction, constrainedSize.min); + const auto maxCrossSize = crossDimension(style.direction, constrainedSize.max); + const CGFloat crossSize = MIN(MAX(minCrossSize, largestChildCrossSize), maxCrossSize); + + /* + Step 5: finally, we must find the smallest descender (descender is negative). This is since ASBaselineLayoutSpec implements + ASLayoutable and needs an ascender and descender to lay itself out properly. + */ + const auto descenderIt = std::max_element(stackedChildren.begin(), stackedChildren.end(), [&](const ASLayout *a, const ASLayout *b){ + return a.position.y + a.size.height < b.position.y + b.size.height; + }); + const CGFloat minDescender = descenderIt == stackedChildren.end() ? 0 : (*descenderIt).layoutableObject.descender; + + return {stackedChildren, crossSize, maxAscender, minDescender}; +} diff --git a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h index 4f5eff0aeb..2b073c106a 100644 --- a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h @@ -10,6 +10,14 @@ #import "ASStackLayoutSpec.h" +typedef struct { + ASStackLayoutDirection direction; + CGFloat spacing; + ASStackLayoutJustifyContent justifyContent; + ASStackLayoutAlignItems alignItems; + BOOL baselineRelativeArrangement; +} ASStackLayoutSpecStyle; + inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) { return (direction == ASStackLayoutDirectionVertical) ? size.height : size.width; @@ -55,6 +63,10 @@ inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, return ASStackLayoutAlignItemsStart; case ASStackLayoutAlignSelfStretch: return ASStackLayoutAlignItemsStretch; + case ASStackLayoutAlignSelfBaselineFirst: + return ASStackLayoutAlignItemsBaselineFirst; + case ASStackLayoutAlignSelfBaselineLast: + return ASStackLayoutAlignItemsBaselineLast; case ASStackLayoutAlignSelfAuto: default: return stackAlignment; diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm index 420258460f..4d23cf6786 100644 --- a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm @@ -14,6 +14,7 @@ #import "ASLayoutSpecUtilities.h" #import "ASStackLayoutSpecUtilities.h" #import "ASLayoutable.h" +#import "ASLayoutOptions.h" static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, const ASStackUnpositionedItem &l, @@ -24,6 +25,8 @@ static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, return crossSize - crossDimension(style.direction, l.layout.size); case ASStackLayoutAlignItemsCenter: return ASFloorPixelValue((crossSize - crossDimension(style.direction, l.layout.size)) / 2); + case ASStackLayoutAlignItemsBaselineFirst: + case ASStackLayoutAlignItemsBaselineLast: case ASStackLayoutAlignItemsStart: case ASStackLayoutAlignItemsStretch: return 0; @@ -44,7 +47,7 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style const auto minCrossSize = crossDimension(style.direction, constrainedSize.min); const auto maxCrossSize = crossDimension(style.direction, constrainedSize.max); const CGFloat crossSize = MIN(MAX(minCrossSize, largestChildCrossSize), maxCrossSize); - + CGPoint p = directionPoint(style.direction, offset, 0); BOOL first = YES; auto stackedChildren = AS::map(unpositionedLayout.items, [&](const ASStackUnpositionedItem &l) -> ASLayout *{ @@ -54,6 +57,7 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style } first = NO; l.layout.position = p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize)); + p = p + directionPoint(style.direction, stackDimension(style.direction, l.layout.size) + l.child.spacingAfter, 0); return l.layout; }); diff --git a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h index 45f648192e..4112af8e66 100644 --- a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h +++ b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h @@ -11,6 +11,7 @@ #import #import "ASLayout.h" +#import "ASStackLayoutSpecUtilities.h" #import "ASStackLayoutSpec.h" struct ASStackUnpositionedItem { diff --git a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm index 9eb83a5b0d..ab2eb3aac2 100644 --- a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm @@ -14,6 +14,7 @@ #import "ASLayoutSpecUtilities.h" #import "ASStackLayoutSpecUtilities.h" +#import "ASLayoutOptions.h" /** Sizes the child given the parameters specified, and returns the computed layout. @@ -297,7 +298,7 @@ static std::vector layoutChildrenAlongUnconstrainedStac const CGFloat exactStackDimension = ASRelativeDimensionResolve(child.flexBasis, stackDimension(style.direction, size)); if (useOptimizedFlexing && isFlexibleInBothDirections(child)) { - return { child, [ASLayout newWithLayoutableObject:child size:{0, 0}] }; + return { child, [ASLayout layoutWithLayoutableObject:child size:{0, 0}] }; } else { return { child, diff --git a/AsyncDisplayKitTests/ASCenterLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASCenterLayoutSpecSnapshotTests.mm index 388e7d0e7e..10c129f10c 100644 --- a/AsyncDisplayKitTests/ASCenterLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASCenterLayoutSpecSnapshotTests.mm @@ -13,6 +13,7 @@ #import "ASBackgroundLayoutSpec.h" #import "ASCenterLayoutSpec.h" #import "ASStackLayoutSpec.h" +#import "ASLayoutOptions.h" static const ASSizeRange kSize = {{100, 120}, {320, 160}}; @@ -52,9 +53,9 @@ static const ASSizeRange kSize = {{100, 120}, {320, 160}}; ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild: + backgroundLayoutSpecWithChild: [ASCenterLayoutSpec - newWithCenteringOptions:options + centerLayoutSpecWithCenteringOptions:options sizingOptions:sizingOptions child:foregroundNode] background:backgroundNode]; @@ -98,11 +99,11 @@ static NSString *suffixForCenteringOptions(ASCenterLayoutSpecCenteringOptions ce ASCenterLayoutSpec *layoutSpec = [ASCenterLayoutSpec - newWithCenteringOptions:ASCenterLayoutSpecCenteringNone + centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringNone sizingOptions:{} child: [ASBackgroundLayoutSpec - newWithChild:[ASStackLayoutSpec newWithStyle:{} children:@[foregroundNode]] + backgroundLayoutSpecWithChild:[ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStart children:@[foregroundNode]] background:backgroundNode]]; [self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:@[backgroundNode, foregroundNode] identifier:nil]; diff --git a/AsyncDisplayKitTests/ASInsetLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASInsetLayoutSpecSnapshotTests.mm index 9ff4bf219e..994acee8c4 100644 --- a/AsyncDisplayKitTests/ASInsetLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASInsetLayoutSpecSnapshotTests.mm @@ -61,7 +61,7 @@ static NSString *nameForInsets(UIEdgeInsets insets) ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild:[ASInsetLayoutSpec newWithInsets:insets child:foregroundNode] + backgroundLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:foregroundNode] background:backgroundNode]; static ASSizeRange kVariableSize = {{0, 0}, {300, 300}}; @@ -82,7 +82,7 @@ static NSString *nameForInsets(UIEdgeInsets insets) ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild:[ASInsetLayoutSpec newWithInsets:insets child:foregroundNode] + backgroundLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:foregroundNode] background:backgroundNode]; static ASSizeRange kFixedSize = {{300, 300}, {300, 300}}; @@ -104,7 +104,7 @@ static NSString *nameForInsets(UIEdgeInsets insets) ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild:[ASInsetLayoutSpec newWithInsets:insets child:foregroundNode] + backgroundLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:foregroundNode] background:backgroundNode]; static ASSizeRange kFixedSize = {{300, 300}, {300, 300}}; diff --git a/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m b/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m index c15b28f6d4..4cd9232f05 100644 --- a/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m +++ b/AsyncDisplayKitTests/ASLayoutSpecSnapshotTestsHelper.m @@ -12,6 +12,7 @@ #import "ASDisplayNode.h" #import "ASLayoutSpec.h" +#import "ASLayout.h" @interface ASTestNode : ASDisplayNode - (void)setLayoutSpecUnderTest:(ASLayoutSpec *)layoutSpecUnderTest sizeRange:(ASSizeRange)sizeRange; @@ -54,7 +55,7 @@ { ASLayout *layout = [layoutSpecUnderTest measureWithSizeRange:sizeRange]; layout.position = CGPointZero; - layout = [ASLayout newWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; + layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; _layoutUnderTest = [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) { return [self.subnodes containsObject:evaluatedLayout.layoutableObject]; }]; diff --git a/AsyncDisplayKitTests/ASOverlayLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASOverlayLayoutSpecSnapshotTests.mm index 066a6c47b5..f024de9faa 100644 --- a/AsyncDisplayKitTests/ASOverlayLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASOverlayLayoutSpecSnapshotTests.mm @@ -34,10 +34,10 @@ static const ASSizeRange kSize = {{320, 320}, {320, 320}}; ASLayoutSpec *layoutSpec = [ASOverlayLayoutSpec - newWithChild:backgroundNode + overlayLayoutSpecWithChild:backgroundNode overlay: [ASCenterLayoutSpec - newWithCenteringOptions:ASCenterLayoutSpecCenteringXY + centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:{} child:foregroundNode]]; diff --git a/AsyncDisplayKitTests/ASRatioLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASRatioLayoutSpecSnapshotTests.mm index ffe30e0f0d..5345a84649 100644 --- a/AsyncDisplayKitTests/ASRatioLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASRatioLayoutSpecSnapshotTests.mm @@ -30,7 +30,7 @@ static const ASSizeRange kFixedSize = {{0, 0}, {100, 100}}; ASStaticSizeDisplayNode *subnode = ASDisplayNodeWithBackgroundColor([UIColor greenColor]); subnode.staticSize = childSize; - ASLayoutSpec *layoutSpec = [ASRatioLayoutSpec newWithRatio:ratio child:subnode]; + ASLayoutSpec *layoutSpec = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:ratio child:subnode]; [self testLayoutSpec:layoutSpec sizeRange:kFixedSize subnodes:@[subnode] identifier:identifier]; } diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index 21a885444e..2252078da6 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -11,9 +11,11 @@ #import "ASLayoutSpecSnapshotTestsHelper.h" #import "ASStackLayoutSpec.h" +#import "ASStackLayoutSpecUtilities.h" #import "ASBackgroundLayoutSpec.h" #import "ASRatioLayoutSpec.h" #import "ASInsetLayoutSpec.h" +#import "ASLayoutOptions.h" @interface ASStackLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase @end @@ -79,7 +81,7 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild:[ASStackLayoutSpec newWithStyle:style children:children] + backgroundLayoutSpecWithChild:[ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems children:children] background:backgroundNode]; NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode]; @@ -179,17 +181,12 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) ASLayoutSpec *layoutSpec = [ASInsetLayoutSpec - newWithInsets:{10, 10, 10 ,10} + insetLayoutSpecWithInsets:{10, 10, 10 ,10} child: [ASBackgroundLayoutSpec - newWithChild: + backgroundLayoutSpecWithChild: [ASStackLayoutSpec - newWithStyle:{ - .direction = ASStackLayoutDirectionVertical, - .spacing = 10, - .alignItems = ASStackLayoutAlignItemsStretch - } - children:@[]] + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical spacing:10 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:@[]] background:backgroundNode]]; // width 300px; height 0-300px @@ -257,7 +254,7 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) ASStaticSizeDisplayNode * subnode2 = ASDisplayNodeWithBackgroundColor([UIColor redColor]); subnode2.staticSize = {50, 50}; - ASRatioLayoutSpec *child1 = [ASRatioLayoutSpec newWithRatio:1.5 child:subnode1]; + ASRatioLayoutSpec *child1 = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.5 child:subnode1]; child1.flexBasis = ASRelativeDimensionMakeWithPercent(1); child1.flexGrow = YES; child1.flexShrink = YES; @@ -481,7 +478,7 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) ((ASStaticSizeDisplayNode *)subnodes[1]).staticSize = {10, 0}; ((ASStaticSizeDisplayNode *)subnodes[2]).staticSize = {3000, 3000}; - ASRatioLayoutSpec *child2 = [ASRatioLayoutSpec newWithRatio:1.0 child:subnodes[2]]; + ASRatioLayoutSpec *child2 = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0 child:subnodes[2]]; child2.flexGrow = YES; child2.flexShrink = YES; @@ -489,16 +486,12 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) // Instead it should be stretched to 300 points tall, matching the red child and not overlapping the green inset. ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec - newWithChild: + backgroundLayoutSpecWithChild: [ASInsetLayoutSpec - newWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) + insetLayoutSpecWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) child: - [ASStackLayoutSpec - newWithStyle:{ - .direction = ASStackLayoutDirectionHorizontal, - .alignItems = ASStackLayoutAlignItemsStretch, - } - children:@[subnodes[1], child2,]]] + [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:@[subnodes[1], child2,]] + ] background:subnodes[0]]; static ASSizeRange kSize = {{300, 0}, {300, INFINITY}}; diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 08531ce131..1d074ac317 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -9,6 +9,7 @@ #import #import "ASTableView.h" +#import "ASDisplayNode+Subclasses.h" #define NumberOfSections 10 #define NumberOfRowsPerSection 20 @@ -56,8 +57,24 @@ @end -@interface ASTableViewFilledDataSource : NSObject +@interface ASTestTextCellNode : ASTextCellNode +/** Calculated by counting how many times -layoutSpecThatFits: is called on the main thread. */ +@property (atomic) int numberOfLayoutsOnMainThread; +@end +@implementation ASTestTextCellNode + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + if ([NSThread isMainThread]) { + _numberOfLayoutsOnMainThread++; + } + return [super layoutSpecThatFits:constrainedSize]; +} + +@end + +@interface ASTableViewFilledDataSource : NSObject @end @implementation ASTableViewFilledDataSource @@ -74,7 +91,7 @@ - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath { - ASTextCellNode *textCellNode = [ASTextCellNode new]; + ASTestTextCellNode *textCellNode = [ASTestTextCellNode new]; textCellNode.text = indexPath.description; return textCellNode; @@ -151,32 +168,49 @@ tableView.asyncDelegate = dataSource; tableView.asyncDataSource = dataSource; + + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; - [tableView reloadData]; + [tableView reloadDataWithCompletion:^{ + NSLog(@"*** Reload Complete ***"); + [reloadDataExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; for (int i = 0; i < NumberOfReloadIterations; ++i) { - UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); - - BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO); + UITableViewRowAnimation rowAnimation = (arc4random_uniform(2) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); + BOOL animatedScroll = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL reloadRowsInsteadOfSections = (arc4random_uniform(2) == 0 ? YES : NO); + NSTimeInterval runLoopDelay = ((arc4random_uniform(2) == 0) ? (1.0 / (1 + arc4random_uniform(500))) : 0); + BOOL useBeginEndUpdates = (arc4random_uniform(3) == 0 ? YES : NO); + + // instrument our instrumentation ;) + //NSLog(@"Iteration %03d: %@|%@|%@|%@|%g", i, (rowAnimation == UITableViewRowAnimationNone) ? @"NONE " : @"MIDDLE", animatedScroll ? @"ASCR" : @" ", reloadRowsInsteadOfSections ? @"ROWS" : @"SECS", useBeginEndUpdates ? @"BEGEND" : @" ", runLoopDelay); if (useBeginEndUpdates) { [tableView beginUpdates]; } if (reloadRowsInsteadOfSections) { - [tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation]; + NSArray *indexPaths = [self randomIndexPathsExisting:YES]; + //NSLog(@"reloading rows: %@", indexPaths); + [tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:rowAnimation]; } else { - [tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation]; + NSIndexSet *sections = [self randomIndexSet]; + //NSLog(@"reloading sections: %@", sections); + [tableView reloadSections:sections withRowAnimation:rowAnimation]; } [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animatedScroll]; - if (letRunloopProceed) { + if (runLoopDelay > 0) { // Run other stuff on the main queue for between 2ms and 1000ms. - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]]; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:runLoopDelay]]; } if (useBeginEndUpdates) { @@ -185,4 +219,213 @@ } } +- (void)testRelayoutAllRowsWithNonZeroSizeInitially +{ + // Initial width of the table view is non-zero and all nodes are measured with this size. + // Any subsequence size change must trigger a relayout. + CGSize tableViewFinalSize = CGSizeMake(100, 500); + // Width and height are swapped so that a later size change will simulate a rotation + ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width) + style:UITableViewStylePlain + asyncDataFetching:YES]; + + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + // Trigger layout measurement on all nodes + [tableView reloadData]; + + [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; +} + +- (void)testRelayoutAllRowsWithZeroSizeInitially +{ + // Initial width of the table view is 0. The first size change is part of the initial config. + // Any subsequence size change after that must trigger a relayout. + CGSize tableViewFinalSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectZero + style:UITableViewStylePlain + asyncDataFetching:YES]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + // Initial configuration + UIView *superview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 500)]; + [superview addSubview:tableView]; + // Width and height are swapped so that a later size change will simulate a rotation + tableView.frame = CGRectMake(0, 0, tableViewFinalSize.height, tableViewFinalSize.width); + // Trigger layout measurement on all nodes + [tableView layoutIfNeeded]; + + [self triggerSizeChangeAndAssertRelayoutAllRowsForTableView:tableView newSize:tableViewFinalSize]; +} + +- (void)triggerSizeChangeAndAssertRelayoutAllRowsForTableView:(ASTableView *)tableView newSize:(CGSize)newSize +{ + XCTestExpectation *nodesMeasuredUsingNewConstrainedSizeExpectation = [self expectationWithDescription:@"nodesMeasuredUsingNewConstrainedSize"]; + + [tableView beginUpdates]; + + CGRect frame = tableView.frame; + frame.size = newSize; + tableView.frame = frame; + [tableView layoutIfNeeded]; + + [tableView endUpdatesAnimated:NO completion:^(BOOL completed) { + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, newSize.width); + } + } + [nodesMeasuredUsingNewConstrainedSizeExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +- (void)testRelayoutVisibleRowsWhenEditingModeIsChanged +{ + CGSize tableViewSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain + asyncDataFetching:YES]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; + [tableView reloadDataWithCompletion:^{ + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + [reloadDataExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + + NSArray *visibleNodes = [tableView visibleNodes]; + XCTAssertGreaterThan(visibleNodes.count, 0); + + // Cause table view to enter editing mode. + // Visibile nodes should be re-measured on main thread with the new (smaller) content view width. + // Other nodes are untouched. + XCTestExpectation *relayoutAfterEnablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterEnablingEditing"]; + [tableView beginUpdates]; + [tableView setEditing:YES]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + if ([visibleNodes containsObject:node]) { + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } else { + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + } + [relayoutAfterEnablingEditingExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + + // Cause table view to leave editing mode. + // Visibile nodes should be re-measured again. + // All nodes should have max constrained width equals to the table view width. + XCTestExpectation *relayoutAfterDisablingEditingExpectation = [self expectationWithDescription:@"relayoutAfterDisablingEditing"]; + [tableView beginUpdates]; + [tableView setEditing:NO]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + BOOL visible = [visibleNodes containsObject:node]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, visible ? 2: 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + [relayoutAfterDisablingEditingExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + +- (void)testRelayoutRowsAfterEditingModeIsChangedAndTheyBecomeVisible +{ + CGSize tableViewSize = CGSizeMake(100, 500); + ASTestTableView *tableView = [[ASTestTableView alloc] initWithFrame:CGRectMake(0, 0, tableViewSize.width, tableViewSize.height) + style:UITableViewStylePlain + asyncDataFetching:YES]; + ASTableViewFilledDataSource *dataSource = [ASTableViewFilledDataSource new]; + + tableView.asyncDelegate = dataSource; + tableView.asyncDataSource = dataSource; + + XCTestExpectation *reloadDataExpectation = [self expectationWithDescription:@"reloadData"]; + [tableView reloadDataWithCompletion:^{ + for (int section = 0; section < NumberOfSections; section++) { + for (int row = 0; row < NumberOfRowsPerSection; row++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section]; + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:indexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 0); + XCTAssertEqual(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + } + } + [reloadDataExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; + + // Cause table view to enter editing mode and then scroll to the bottom. + // The last node should be re-measured on main thread with the new (smaller) content view width. + NSIndexPath *lastRowIndexPath = [NSIndexPath indexPathForRow:(NumberOfRowsPerSection - 1) inSection:(NumberOfSections - 1)]; + XCTestExpectation *relayoutExpectation = [self expectationWithDescription:@"relayout"]; + [tableView beginUpdates]; + [tableView setEditing:YES]; + [tableView setContentOffset:CGPointMake(0, CGFLOAT_MAX) animated:YES]; + [tableView endUpdatesAnimated:YES completion:^(BOOL completed) { + ASTestTextCellNode *node = (ASTestTextCellNode *)[tableView nodeForRowAtIndexPath:lastRowIndexPath]; + XCTAssertEqual(node.numberOfLayoutsOnMainThread, 1); + XCTAssertLessThan(node.constrainedSizeForCalculatedLayout.max.width, tableViewSize.width); + [relayoutExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + @end diff --git a/AsyncDisplayKitTests/ASTextNodeRendererTests.m b/AsyncDisplayKitTests/ASTextNodeRendererTests.m index a9bf427af4..bcfb7c12ce 100644 --- a/AsyncDisplayKitTests/ASTextNodeRendererTests.m +++ b/AsyncDisplayKitTests/ASTextNodeRendererTests.m @@ -69,6 +69,22 @@ XCTAssertTrue(size.height > 0, @"Should have a nonzero height"); } +- (void)testCalculateSizeWithEmptyString +{ + _attributedString = [[NSAttributedString alloc] initWithString:@""]; + [self setUpRenderer]; + CGSize size = [_renderer size]; + XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, size), @"Empty NSAttributedString should result in CGSizeZero"); +} + +- (void)testCalculateSizeWithNilString +{ + _attributedString = nil; + [self setUpRenderer]; + CGSize size = [_renderer size]; + XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, size), @"Nil NSAttributedString should result in CGSizeZero"); +} + - (void)testNumberOfLines { [self setUpRenderer]; diff --git a/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm b/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm index dc683fd677..4768197ce0 100644 --- a/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm +++ b/AsyncDisplayKitTests/ASTextNodeWordKernerTests.mm @@ -80,7 +80,7 @@ CGFloat expectedWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width; CGRect boundingBox = [_layoutManagerDelegate layoutManager:_components.layoutManager boundingBoxForControlGlyphAtIndex:0 forTextContainer:_components.textContainer proposedLineFragment:CGRectZero glyphPosition:CGPointZero characterIndex:0]; - + XCTAssertEqualWithAccuracy(boundingBox.size.width, expectedWidth, FLT_EPSILON, @"Word kerning shouldn't alter the default width of %f. Encountered space width was %f", expectedWidth, boundingBox.size.width); } @@ -111,7 +111,7 @@ } NSGlyphProperty *glyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount); CGGlyph *glyphs = (CGGlyph *)malloc(sizeof(CGGlyph) * glyphCount); - NSInteger glyphsToChange = [_layoutManagerDelegate layoutManager:_components.layoutManager shouldGenerateGlyphs:glyphs properties:glyphProperties characterIndexes:characterIndexes font:NULL forGlyphRange:stringRange]; + NSInteger glyphsToChange = [_layoutManagerDelegate layoutManager:_components.layoutManager shouldGenerateGlyphs:glyphs properties:glyphProperties characterIndexes:characterIndexes font:[UIFont systemFontOfSize:12.0] forGlyphRange:stringRange]; free(characterIndexes); free(glyphProperties); free(glyphs); diff --git a/Podfile b/Podfile index eb7cb4bb47..687ae3c56c 100644 --- a/Podfile +++ b/Podfile @@ -4,5 +4,5 @@ platform :ios, '7.0' target :'AsyncDisplayKitTests', :exclusive => true do pod 'OCMock', '~> 2.2' - pod 'FBSnapshotTestCase' + pod 'FBSnapshotTestCase', '~> 1.8.1' end diff --git a/Podfile.lock b/Podfile.lock index 4196f05616..423c2d2851 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,7 +3,7 @@ PODS: - OCMock (2.2.4) DEPENDENCIES: - - FBSnapshotTestCase + - FBSnapshotTestCase (~> 1.8.1) - OCMock (~> 2.2) SPEC CHECKSUMS: diff --git a/docs/build.sh b/docs/build.sh index 6605748f6f..c532bb0d6b 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -HEADERS=`ls ../AsyncDisplayKit/*.h ../AsyncDisplayKit/Details/ASRangeController.h` +HEADERS=`ls ../AsyncDisplayKit/*.h ../AsyncDisplayKit/Details/ASRangeController.h ../AsyncDisplayKit/Layout/*.h` rm -rf htdocs appledoc diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 5e2ab4292a..f87e26f9d7 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -95,4 +95,8 @@ [context completeBatchFetching:YES]; } +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + return UIEdgeInsetsMake(20.0, 20.0, 20.0, 20.0); +} + @end diff --git a/examples/ASTableViewStressTest/Sample/ViewController.m b/examples/ASTableViewStressTest/Sample/ViewController.m index 92e3ce7e33..a297d37c42 100644 --- a/examples/ASTableViewStressTest/Sample/ViewController.m +++ b/examples/ASTableViewStressTest/Sample/ViewController.m @@ -110,11 +110,14 @@ for (int i = 0; i < NumberOfReloadIterations; ++i) { UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); - BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL addIndexPaths = (arc4random_uniform(1) == 0 ? YES : NO); - BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL animatedScroll = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL reloadRowsInsteadOfSections = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL letRunloopProceed = (arc4random_uniform(2) == 0 ? YES : NO); + BOOL useBeginEndUpdates = (arc4random_uniform(3) == 0 ? YES : NO); + + // FIXME: Need to revise the logic to support mutating the data source rather than just reload thrashing. + // UITableView itself does not support deleting a row in the same edit transaction as reloading it, for example. + BOOL addIndexPaths = NO; //(arc4random_uniform(2) == 0 ? YES : NO); if (useBeginEndUpdates) { [_tableView beginUpdates]; diff --git a/examples/EditableText/Sample/ViewController.m b/examples/EditableText/Sample/ViewController.m index 595458ec85..91288dfad5 100644 --- a/examples/EditableText/Sample/ViewController.m +++ b/examples/EditableText/Sample/ViewController.m @@ -31,6 +31,7 @@ // simple editable text node. here we use it synchronously, but it fully supports async layout & display _textNode = [[ASEditableTextNode alloc] init]; + _textNode.returnKeyType = UIReturnKeyDone; _textNode.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:0.1f]; // with placeholder text (displayed if the user hasn't entered text) diff --git a/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png b/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png new file mode 100644 index 0000000000..6ee80b9393 Binary files /dev/null and b/examples/HorizontalWithinVerticalScrolling/Default-568h@2x.png differ diff --git a/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png b/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png new file mode 100644 index 0000000000..e7b975e21b Binary files /dev/null and b/examples/HorizontalWithinVerticalScrolling/Default-667h@2x.png differ diff --git a/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png b/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png new file mode 100644 index 0000000000..c8949cae16 Binary files /dev/null and b/examples/HorizontalWithinVerticalScrolling/Default-736h@3x.png differ diff --git a/examples/HorizontalWithinVerticalScrolling/Podfile b/examples/HorizontalWithinVerticalScrolling/Podfile new file mode 100644 index 0000000000..6c012e3c04 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Podfile @@ -0,0 +1,3 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +pod 'AsyncDisplayKit', :path => '../..' diff --git a/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..ba7ab3428a --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.pbxproj @@ -0,0 +1,358 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05561CFD19D4F94A00CBA93C /* HorizontalScrollCellNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */; }; + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */; }; + 05E2128719D4DB510098F589 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128619D4DB510098F589 /* main.m */; }; + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128919D4DB510098F589 /* AppDelegate.m */; }; + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E2128C19D4DB510098F589 /* ViewController.m */; }; + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */; }; + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */; }; + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AA19EE274300767484 /* Default-667h@2x.png */; }; + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6C2C82AB19EE274300767484 /* Default-736h@3x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 05561CFB19D4F94A00CBA93C /* HorizontalScrollCellNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HorizontalScrollCellNode.h; sourceTree = ""; }; + 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = HorizontalScrollCellNode.mm; sourceTree = ""; }; + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "../Default-568h@2x.png"; sourceTree = ""; }; + 05E2128119D4DB510098F589 /* Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E2128519D4DB510098F589 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 05E2128619D4DB510098F589 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 05E2128819D4DB510098F589 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 05E2128919D4DB510098F589 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 05E2128B19D4DB510098F589 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 05E2128C19D4DB510098F589 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; }; + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RandomCoreGraphicsNode.h; sourceTree = ""; }; + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RandomCoreGraphicsNode.m; sourceTree = ""; }; + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-667h@2x.png"; sourceTree = SOURCE_ROOT; }; + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-736h@3x.png"; sourceTree = SOURCE_ROOT; }; + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 05E2127E19D4DB510098F589 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EC0CDCBA10D483D9F386E5E /* libPods.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05E2127819D4DB510098F589 = { + isa = PBXGroup; + children = ( + 05E2128319D4DB510098F589 /* Sample */, + 05E2128219D4DB510098F589 /* Products */, + 1A943BF0259746F18D6E423F /* Frameworks */, + 1AE410B73DA5C3BD087ACDD7 /* Pods */, + ); + sourceTree = ""; + }; + 05E2128219D4DB510098F589 /* Products */ = { + isa = PBXGroup; + children = ( + 05E2128119D4DB510098F589 /* Sample.app */, + ); + name = Products; + sourceTree = ""; + }; + 05E2128319D4DB510098F589 /* Sample */ = { + isa = PBXGroup; + children = ( + 05E2128819D4DB510098F589 /* AppDelegate.h */, + 05E2128919D4DB510098F589 /* AppDelegate.m */, + 05E2128B19D4DB510098F589 /* ViewController.h */, + 05E2128C19D4DB510098F589 /* ViewController.m */, + 05561CFB19D4F94A00CBA93C /* HorizontalScrollCellNode.h */, + 05561CFC19D4F94A00CBA93C /* HorizontalScrollCellNode.mm */, + 18C2ED841B9B8CE700F627B3 /* RandomCoreGraphicsNode.h */, + 18C2ED851B9B8CE700F627B3 /* RandomCoreGraphicsNode.m */, + 05E2128419D4DB510098F589 /* Supporting Files */, + ); + path = Sample; + sourceTree = ""; + }; + 05E2128419D4DB510098F589 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0585427F19D4DBE100606EA6 /* Default-568h@2x.png */, + 6C2C82AA19EE274300767484 /* Default-667h@2x.png */, + 6C2C82AB19EE274300767484 /* Default-736h@3x.png */, + 05E2128519D4DB510098F589 /* Info.plist */, + 05E2128619D4DB510098F589 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1A943BF0259746F18D6E423F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 3D24B17D1E4A4E7A9566C5E9 /* libPods.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1AE410B73DA5C3BD087ACDD7 /* Pods */ = { + isa = PBXGroup; + children = ( + C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */, + 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 05E2128019D4DB510098F589 /* Sample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */; + buildPhases = ( + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */, + 05E2127D19D4DB510098F589 /* Sources */, + 05E2127E19D4DB510098F589 /* Frameworks */, + 05E2127F19D4DB510098F589 /* Resources */, + F012A6F39E0149F18F564F50 /* Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sample; + productName = Sample; + productReference = 05E2128119D4DB510098F589 /* Sample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 05E2127919D4DB510098F589 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 05E2128019D4DB510098F589 = { + CreatedOnToolsVersion = 6.0.1; + }; + }; + }; + buildConfigurationList = 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 05E2127819D4DB510098F589; + productRefGroup = 05E2128219D4DB510098F589 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 05E2128019D4DB510098F589 /* Sample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 05E2127F19D4DB510098F589 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0585428019D4DBE100606EA6 /* Default-568h@2x.png in Resources */, + 6C2C82AC19EE274300767484 /* Default-667h@2x.png in Resources */, + 6C2C82AD19EE274300767484 /* Default-736h@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + E080B80F89C34A25B3488E26 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + F012A6F39E0149F18F564F50 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods/Pods-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 05E2127D19D4DB510098F589 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 18C2ED861B9B8CE700F627B3 /* RandomCoreGraphicsNode.m in Sources */, + 05561CFD19D4F94A00CBA93C /* HorizontalScrollCellNode.mm in Sources */, + 05E2128D19D4DB510098F589 /* ViewController.m in Sources */, + 05E2128A19D4DB510098F589 /* AppDelegate.m in Sources */, + 05E2128719D4DB510098F589 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 05E212A219D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 05E212A319D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 05E212A519D4DB510098F589 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C068F1D3F0CC317E895FCDAB /* Pods.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05E212A619D4DB510098F589 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 088AA6578212BE9BFBB07B70 /* Pods.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = Sample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 7.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 05E2127C19D4DB510098F589 /* Build configuration list for PBXProject "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A219D4DB510098F589 /* Debug */, + 05E212A319D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E212A419D4DB510098F589 /* Build configuration list for PBXNativeTarget "Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E212A519D4DB510098F589 /* Debug */, + 05E212A619D4DB510098F589 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 05E2127919D4DB510098F589 /* Project object */; +} diff --git a/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a80c038249 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme new file mode 100644 index 0000000000..1e14aa0329 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/HorizontalWithinVerticalScrolling/Sample.xcworkspace/contents.xcworkspacedata b/examples/HorizontalWithinVerticalScrolling/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..d98549fd35 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h b/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h new file mode 100644 index 0000000000..85855277e9 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.h @@ -0,0 +1,20 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +#define UseAutomaticLayout 1 + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m b/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m new file mode 100644 index 0000000000..1dea563b77 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/AppDelegate.m @@ -0,0 +1,27 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "AppDelegate.h" + +#import "ViewController.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + self.window.backgroundColor = [UIColor whiteColor]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h b/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h new file mode 100644 index 0000000000..9d036ce48e --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.h @@ -0,0 +1,22 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +/** + * This ASCellNode contains an ASCollectionNode. It intelligently interacts with a containing ASCollectionView or ASTableView, + * to preload and clean up contents as the user scrolls around both vertically and horizontally — in a way that minimizes memory usage. + */ +@interface HorizontalScrollCellNode : ASCellNode + +- (instancetype)initWithElementSize:(CGSize)size; + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm b/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm new file mode 100644 index 0000000000..9eb2ee9b31 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/HorizontalScrollCellNode.mm @@ -0,0 +1,100 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import "HorizontalScrollCellNode.h" +#import "RandomCoreGraphicsNode.h" +#import "AppDelegate.h" + +#import + +#import +#import + +static const CGFloat kOuterPadding = 16.0f; +static const CGFloat kInnerPadding = 10.0f; + +@interface HorizontalScrollCellNode () +{ + ASCollectionNode *_collectionNode; + CGSize _elementSize; + ASDisplayNode *_divider; +} + +@end + + +@implementation HorizontalScrollCellNode + +- (instancetype)initWithElementSize:(CGSize)size +{ + if (!(self = [super init])) + return nil; + + _elementSize = size; + + UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init]; + flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; + flowLayout.itemSize = _elementSize; + flowLayout.minimumInteritemSpacing = kInnerPadding; + _collectionNode = [[ASCollectionNode alloc] initWithCollectionViewLayout:flowLayout]; + [self addSubnode:_collectionNode]; + + // hairline cell separator + _divider = [[ASDisplayNode alloc] init]; + _divider.backgroundColor = [UIColor lightGrayColor]; + [self addSubnode:_divider]; + + return self; +} + +- (void)didLoad +{ + [super didLoad]; + _collectionNode.view.asyncDelegate = self; + _collectionNode.view.asyncDataSource = self; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + return 5; +} + +- (ASCellNode *)collectionView:(ASCollectionView *)collectionView nodeForItemAtIndexPath:(NSIndexPath *)indexPath +{ + RandomCoreGraphicsNode *elementNode = [[RandomCoreGraphicsNode alloc] init]; + elementNode.preferredFrameSize = _elementSize; + return elementNode; +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + _collectionNode.preferredFrameSize = CGSizeMake(self.bounds.size.width, _elementSize.height); + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, 0.0, kOuterPadding, 0.0); + insetSpec.child = _collectionNode; + + return insetSpec; +} + +// With box model, you don't need to override this method, unless you want to add custom logic. +- (void)layout +{ + [super layout]; + + _collectionNode.view.contentInset = UIEdgeInsetsMake(0.0, kOuterPadding, 0.0, kOuterPadding); + + // Manually layout the divider. + CGFloat pixelHeight = 1.0f / [[UIScreen mainScreen] scale]; + _divider.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, pixelHeight); +} + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist b/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist new file mode 100644 index 0000000000..35d842827b --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.AsyncDisplayKit.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h b/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h new file mode 100644 index 0000000000..f4324b5d3e --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.h @@ -0,0 +1,13 @@ +// +// RandomCoreGraphicsNode.h +// Sample +// +// Created by Scott Goodson on 9/5/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +@interface RandomCoreGraphicsNode : ASCellNode + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m b/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m new file mode 100644 index 0000000000..56435a80df --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/RandomCoreGraphicsNode.m @@ -0,0 +1,44 @@ +// +// RandomCoreGraphicsNode.m +// Sample +// +// Created by Scott Goodson on 9/5/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RandomCoreGraphicsNode.h" +#import + +@implementation RandomCoreGraphicsNode + ++ (UIColor *)randomColor +{ + CGFloat hue = ( arc4random() % 256 / 256.0 ); // 0.0 to 1.0 + CGFloat saturation = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from white + CGFloat brightness = ( arc4random() % 128 / 256.0 ) + 0.5; // 0.5 to 1.0, away from black + return [UIColor colorWithHue:hue saturation:saturation brightness:brightness alpha:1]; +} + ++ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing +{ + CGFloat locations[3]; + NSMutableArray *colors = [NSMutableArray arrayWithCapacity:3]; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[0] = 0.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[1] = 1.0; + [colors addObject:(id)[[RandomCoreGraphicsNode randomColor] CGColor]]; + locations[2] = ( arc4random() % 256 / 256.0 ); + + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)colors, locations); + + CGGradientDrawingOptions drawingOptions; + CGContextDrawLinearGradient(ctx, gradient, CGPointZero, CGPointMake(bounds.size.width, bounds.size.height), drawingOptions); + + CGColorSpaceRelease(colorSpace); +} + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h b/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h new file mode 100644 index 0000000000..d0e9200d88 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.h @@ -0,0 +1,16 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +@interface ViewController : UIViewController + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m b/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m new file mode 100644 index 0000000000..b8297f5fe6 --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/ViewController.m @@ -0,0 +1,84 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import + +#import "ViewController.h" +#import "HorizontalScrollCellNode.h" + +@interface ViewController () +{ + ASTableView *_tableView; +} + +@end + +@implementation ViewController + +#pragma mark - +#pragma mark UIViewController. + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + _tableView.asyncDataSource = self; + _tableView.asyncDelegate = self; + + self.title = @"Horizontal Scrolling Gradients"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRedo + target:self + action:@selector(reloadEverything)]; + + return self; +} + +- (void)reloadEverything +{ + [_tableView reloadData]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self.view addSubview:_tableView]; +} + +- (void)viewWillLayoutSubviews +{ + _tableView.frame = self.view.bounds; +} + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +#pragma mark - +#pragma mark ASTableView. + +- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath +{ + HorizontalScrollCellNode *node = [[HorizontalScrollCellNode alloc] initWithElementSize:CGSizeMake(100, 100)]; + return node; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return 100; +} + +@end diff --git a/examples/HorizontalWithinVerticalScrolling/Sample/main.m b/examples/HorizontalWithinVerticalScrolling/Sample/main.m new file mode 100644 index 0000000000..ae9488711c --- /dev/null +++ b/examples/HorizontalWithinVerticalScrolling/Sample/main.m @@ -0,0 +1,20 @@ +/* This file provided by Facebook is for non-commercial testing and evaluation + * purposes only. Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/examples/Kittens/Sample.xcodeproj/project.pbxproj b/examples/Kittens/Sample.xcodeproj/project.pbxproj index fde58763d9..1e08bbe8c1 100644 --- a/examples/Kittens/Sample.xcodeproj/project.pbxproj +++ b/examples/Kittens/Sample.xcodeproj/project.pbxproj @@ -58,7 +58,9 @@ 1A943BF0259746F18D6E423F /* Frameworks */, 1AE410B73DA5C3BD087ACDD7 /* Pods */, ); + indentWidth = 2; sourceTree = ""; + tabWidth = 2; }; 05E2128219D4DB510098F589 /* Products */ = { isa = PBXGroup; @@ -314,6 +316,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 7.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -326,6 +329,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 7.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/examples/Kittens/Sample/AppDelegate.m b/examples/Kittens/Sample/AppDelegate.m index a8e5594780..1dea563b77 100644 --- a/examples/Kittens/Sample/AppDelegate.m +++ b/examples/Kittens/Sample/AppDelegate.m @@ -19,7 +19,7 @@ { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; - self.window.rootViewController = [[ViewController alloc] init]; + self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:[[ViewController alloc] init]]; [self.window makeKeyAndVisible]; return YES; } diff --git a/examples/Kittens/Sample/BlurbNode.m b/examples/Kittens/Sample/BlurbNode.m index c26cecacad..693ec0cd03 100644 --- a/examples/Kittens/Sample/BlurbNode.m +++ b/examples/Kittens/Sample/BlurbNode.m @@ -74,16 +74,15 @@ static NSString *kLinkAttributeName = @"PlaceKittenNodeLinkAttributeName"; } #if UseAutomaticLayout -- (id)layoutSpecThatFits:(ASSizeRange)constrainedSize +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { - return - [ASInsetLayoutSpec - newWithInsets:UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding) - child: - [ASCenterLayoutSpec - newWithCenteringOptions:ASCenterLayoutSpecCenteringX // Center the text horizontally - sizingOptions:ASCenterLayoutSpecSizingOptionMinimumY // Takes up minimum height - child:_textNode]]; + ASCenterLayoutSpec *centerSpec = [[ASCenterLayoutSpec alloc] init]; + centerSpec.centeringOptions = ASCenterLayoutSpecCenteringX; + centerSpec.sizingOptions = ASCenterLayoutSpecSizingOptionMinimumY; + centerSpec.child = _textNode; + + UIEdgeInsets padding =UIEdgeInsetsMake(kTextPadding, kTextPadding, kTextPadding, kTextPadding); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:padding child:centerSpec]; } #else - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize diff --git a/examples/Kittens/Sample/KittenNode.h b/examples/Kittens/Sample/KittenNode.h index 162cb22a32..3cc23d5a44 100644 --- a/examples/Kittens/Sample/KittenNode.h +++ b/examples/Kittens/Sample/KittenNode.h @@ -19,4 +19,6 @@ - (instancetype)initWithKittenOfSize:(CGSize)size; +- (void)toggleImageEnlargement; + @end diff --git a/examples/Kittens/Sample/KittenNode.mm b/examples/Kittens/Sample/KittenNode.mm index db435d40ef..d1f49351a2 100644 --- a/examples/Kittens/Sample/KittenNode.mm +++ b/examples/Kittens/Sample/KittenNode.mm @@ -29,6 +29,8 @@ static const CGFloat kInnerPadding = 10.0f; ASNetworkImageNode *_imageNode; ASTextNode *_textNode; ASDisplayNode *_divider; + BOOL _isImageEnlarged; + BOOL _swappedTextAndImage; } @end @@ -84,6 +86,7 @@ static const CGFloat kInnerPadding = 10.0f; (NSInteger)roundl(_kittenSize.width), (NSInteger)roundl(_kittenSize.height)]]; // _imageNode.contentMode = UIViewContentModeCenter; + [_imageNode addTarget:self action:@selector(toggleNodesSwap) forControlEvents:ASControlNodeEventTouchUpInside]; [self addSubnode:_imageNode]; // lorem ipsum text, plus some nice styling @@ -129,23 +132,21 @@ static const CGFloat kInnerPadding = 10.0f; } #if UseAutomaticLayout -- (id)layoutSpecThatFits:(ASSizeRange)constrainedSize +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { - ASRatioLayoutSpec *imagePlaceholder = [ASRatioLayoutSpec newWithRatio:1.0 child:_imageNode]; - imagePlaceholder.flexBasis = ASRelativeDimensionMakeWithPoints(kImageSize); - + _imageNode.preferredFrameSize = _isImageEnlarged ? CGSizeMake(2.0 * kImageSize, 2.0 * kImageSize) : CGSizeMake(kImageSize, kImageSize); _textNode.flexShrink = YES; - return - [ASInsetLayoutSpec - newWithInsets:UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding) - child: - [ASStackLayoutSpec - newWithStyle:{ - .direction = ASStackLayoutDirectionHorizontal, - .spacing = kInnerPadding - } - children:@[imagePlaceholder, _textNode]]]; + ASStackLayoutSpec *stackSpec = [[ASStackLayoutSpec alloc] init]; + stackSpec.direction = ASStackLayoutDirectionHorizontal; + stackSpec.spacing = kInnerPadding; + [stackSpec setChildren:!_swappedTextAndImage ? @[_imageNode, _textNode] : @[_textNode, _imageNode]]; + + ASInsetLayoutSpec *insetSpec = [[ASInsetLayoutSpec alloc] init]; + insetSpec.insets = UIEdgeInsetsMake(kOuterPadding, kOuterPadding, kOuterPadding, kOuterPadding); + insetSpec.child = stackSpec; + + return insetSpec; } // With box model, you don't need to override this method, unless you want to add custom logic. @@ -181,4 +182,16 @@ static const CGFloat kInnerPadding = 10.0f; } #endif +- (void)toggleImageEnlargement +{ + _isImageEnlarged = !_isImageEnlarged; + [self setNeedsLayout]; +} + +- (void)toggleNodesSwap +{ + _swappedTextAndImage = !_swappedTextAndImage; + [self setNeedsLayout]; +} + @end diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index b3293c13fc..7989fe15ce 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -27,12 +27,13 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell ASTableView *_tableView; // array of boxed CGSizes corresponding to placekitten.com kittens - NSArray *_kittenDataSource; + NSMutableArray *_kittenDataSource; BOOL _dataSourceLocked; + NSIndexPath *_blurbNodeIndexPath; } -@property (nonatomic, strong) NSArray *kittenDataSource; +@property (nonatomic, strong) NSMutableArray *kittenDataSource; @property (atomic, assign) BOOL dataSourceLocked; @end @@ -56,10 +57,17 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell // populate our "data source" with some random kittens _kittenDataSource = [self createLitterWithSize:kLitterSize]; + _blurbNodeIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; + + self.title = @"Kittens"; + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit + target:self + action:@selector(toggleEditingMode)]; + return self; } -- (NSArray *)createLitterWithSize:(NSInteger)litterSize +- (NSMutableArray *)createLitterWithSize:(NSInteger)litterSize { NSMutableArray *kittens = [NSMutableArray arrayWithCapacity:litterSize]; for (NSInteger i = 0; i < litterSize; i++) { @@ -75,7 +83,7 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell return kittens; } -- (void)setKittenDataSource:(NSArray *)kittenDataSource { +- (void)setKittenDataSource:(NSMutableArray *)kittenDataSource { ASDisplayNodeAssert(!self.dataSourceLocked, @"Could not update data source when it is locked !"); _kittenDataSource = kittenDataSource; @@ -98,14 +106,29 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell return YES; } +- (void)toggleEditingMode +{ + [_tableView setEditing:!_tableView.editing animated:YES]; +} + #pragma mark - #pragma mark ASTableView. +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + [_tableView deselectRowAtIndexPath:indexPath animated:YES]; + [_tableView beginUpdates]; + // Assume only kitten nodes are selectable (see -tableView:shouldHighlightRowAtIndexPath:). + KittenNode *node = (KittenNode *)[_tableView nodeForRowAtIndexPath:indexPath]; + [node toggleImageEnlargement]; + [_tableView endUpdates]; +} + - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath { // special-case the first row - if (indexPath.section == 0 && indexPath.row == 0) { + if ([_blurbNodeIndexPath compare:indexPath] == NSOrderedSame) { BlurbNode *node = [[BlurbNode alloc] init]; return node; } @@ -123,8 +146,8 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { - // disable row selection - return NO; + // Enable selection for kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; } - (void)tableViewLockDataSource:(ASTableView *)tableView @@ -163,7 +186,7 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell } // add new kittens to the data source & notify table of new indexpaths - _kittenDataSource = [_kittenDataSource arrayByAddingObjectsFromArray:moarKittens]; + [_kittenDataSource addObjectsFromArray:moarKittens]; [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; [context completeBatchFetching:YES]; @@ -173,4 +196,19 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell }); } +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Enable editing for Kitten nodes + return [_blurbNodeIndexPath compare:indexPath] != NSOrderedSame; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Assume only kitten nodes are editable (see -tableView:canEditRowAtIndexPath:). + [_kittenDataSource removeObjectAtIndex:indexPath.row - 1]; + [_tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + } +} + @end