Repository: Instagram/IGListKit Branch: main Commit: 01887c6e16e0 Files: 574 Total size: 4.8 MB Directory structure: gitextract_na4i75rs/ ├── .github/ │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── RELEASE_CHECKLIST.md │ └── workflows/ │ └── CI.yml ├── .gitignore ├── .slather.yml ├── .swiftlint.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dangerfile ├── Examples/ │ ├── Examples-iOS/ │ │ ├── IGListKitExamples/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── LaunchScreen.storyboard │ │ │ ├── DelegateProtocols/ │ │ │ │ └── PostSectionControllerDelegate.swift │ │ │ ├── Extensions/ │ │ │ │ ├── UIActivityIndicatorView+Extension.swift │ │ │ │ └── UIColor+Extension.swift │ │ │ ├── IGListKitExamples.entitlements │ │ │ ├── Info.plist │ │ │ ├── Models/ │ │ │ │ ├── APIService.swift │ │ │ │ ├── ActivityItem.swift │ │ │ │ ├── FeedItem.swift │ │ │ │ ├── GridItem.swift │ │ │ │ ├── HorizontalCardsSection.swift │ │ │ │ ├── LoadingCellModel.swift │ │ │ │ ├── Month.swift │ │ │ │ ├── Post.h │ │ │ │ ├── Post.m │ │ │ │ ├── PostModel.swift │ │ │ │ ├── RemodelGeneratedModels/ │ │ │ │ │ ├── PersonModel.h │ │ │ │ │ ├── PersonModel.m │ │ │ │ │ └── PersonModel.value │ │ │ │ ├── SelectionModel.swift │ │ │ │ ├── SwipeActionSection.swift │ │ │ │ ├── User.swift │ │ │ │ └── ViewModels/ │ │ │ │ ├── DayViewModel.swift │ │ │ │ └── MonthTitleViewModel.swift │ │ │ ├── SectionControllers/ │ │ │ │ ├── DemoSectionController.swift │ │ │ │ ├── DisplaySectionController.swift │ │ │ │ ├── EmbeddedSectionController.swift │ │ │ │ ├── ExpandableSectionController.swift │ │ │ │ ├── FeedItemSectionController.swift │ │ │ │ ├── GridSectionController.swift │ │ │ │ ├── HorizontalSectionController.swift │ │ │ │ ├── LabelSectionController.swift │ │ │ │ ├── ListeningSectionController.swift │ │ │ │ ├── LoadingSectionController.swift │ │ │ │ ├── MonthSectionController.swift │ │ │ │ ├── PersonSectionController.h │ │ │ │ ├── PersonSectionController.m │ │ │ │ ├── PostSectionController.h │ │ │ │ ├── PostSectionController.m │ │ │ │ ├── PostSectionController.swift │ │ │ │ ├── RemoveSectionController.swift │ │ │ │ ├── ReorderableSectionController.swift │ │ │ │ ├── SearchSectionController.swift │ │ │ │ ├── SelfSizingSectionController.swift │ │ │ │ ├── StoryboardLabelSectionController.swift │ │ │ │ ├── UserSectionController.swift │ │ │ │ ├── With Composable Layout/ │ │ │ │ │ ├── ActivityComposableSectionController.swift │ │ │ │ │ ├── ExpandableComposableSectionController.swift │ │ │ │ │ ├── GridComposableSectionController.swift │ │ │ │ │ ├── HorizontalComposableSectionController.swift │ │ │ │ │ ├── SelectionComposableSectionController.swift │ │ │ │ │ ├── SwipeActionComposabelSectionController.swift │ │ │ │ │ └── UserComposableSectionController.swift │ │ │ │ └── WorkingRangeSectionController.swift │ │ │ ├── Storyboard/ │ │ │ │ └── Demo.storyboard │ │ │ ├── Systems/ │ │ │ │ └── IncrementAnnouncer.swift │ │ │ ├── ViewControllers/ │ │ │ │ ├── AnnouncingDepsViewController.swift │ │ │ │ ├── CalendarViewController.swift │ │ │ │ ├── CompositionLayoutViewController.swift │ │ │ │ ├── DemosViewController.swift │ │ │ │ ├── DiffTableViewController.swift │ │ │ │ ├── DisplayViewController.swift │ │ │ │ ├── EmptyViewController.swift │ │ │ │ ├── FeedViewController.swift │ │ │ │ ├── IGListKitExamples-Bridging-Header.h │ │ │ │ ├── LoadMoreViewController.swift │ │ │ │ ├── MixedDataViewController.swift │ │ │ │ ├── NestedAdapterViewController.swift │ │ │ │ ├── ObjcDemoViewController.h │ │ │ │ ├── ObjcDemoViewController.m │ │ │ │ ├── ObjcGeneratedModelDemoViewController.h │ │ │ │ ├── ObjcGeneratedModelDemoViewController.m │ │ │ │ ├── ReorderableViewController.swift │ │ │ │ ├── SearchViewController.swift │ │ │ │ ├── SelfSizingCellsViewController.swift │ │ │ │ ├── SingleSectionStoryboardViewController.swift │ │ │ │ ├── SingleSectionViewController.swift │ │ │ │ ├── StoryboardViewController.swift │ │ │ │ ├── SupplementaryViewController.swift │ │ │ │ └── WorkingRangeViewController.swift │ │ │ └── Views/ │ │ │ ├── CalendarDayCell.swift │ │ │ ├── CenterLabelCell.swift │ │ │ ├── CommentCell.h │ │ │ ├── CommentCell.m │ │ │ ├── CompositionLayoutCell.swift │ │ │ ├── DetailLabelCell.swift │ │ │ ├── EmbeddedCollectionViewCell.swift │ │ │ ├── FullWidthSelfSizingCell.swift │ │ │ ├── ImageCell.swift │ │ │ ├── InteractiveCell.h │ │ │ ├── InteractiveCell.m │ │ │ ├── LabelCell.swift │ │ │ ├── LoadingCell.swift │ │ │ ├── ManuallySelfSizingCell.swift │ │ │ ├── MonthTitleCell.swift │ │ │ ├── NibCell.swift │ │ │ ├── NibCell.xib │ │ │ ├── NibSelfSizingCell.swift │ │ │ ├── NibSelfSizingCell.xib │ │ │ ├── PersonCell.h │ │ │ ├── PersonCell.m │ │ │ ├── PhotoCell.h │ │ │ ├── PhotoCell.m │ │ │ ├── PostCell.swift │ │ │ ├── RemoveCell.swift │ │ │ ├── SearchCell.swift │ │ │ ├── SpinnerCell.swift │ │ │ ├── StoryboardCell.swift │ │ │ ├── UserFooterView.swift │ │ │ ├── UserFooterView.xib │ │ │ ├── UserHeaderView.swift │ │ │ ├── UserHeaderView.xib │ │ │ ├── UserInfoCell.h │ │ │ └── UserInfoCell.m │ │ ├── IGListKitExamples-UITests/ │ │ │ ├── DemosViewControllerUITests.swift │ │ │ ├── FeedViewController.swift │ │ │ ├── Info.plist │ │ │ ├── LoadMoreViewControllerUITests.swift │ │ │ ├── MixedDataViewControllerUITests.swift │ │ │ ├── SearchViewControllerUITests.swift │ │ │ └── UITestCase.swift │ │ ├── IGListKitExamples.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ ├── IGListKitExamples.xcscheme │ │ │ ├── IGListKitMessageExample.xcscheme │ │ │ └── IGListKitTodayExample.xcscheme │ │ ├── IGListKitMessageExample/ │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── Contents.json │ │ │ │ └── iMessage App Icon.stickersiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ └── MessagesViewController.swift │ │ ├── IGListKitTodayExample/ │ │ │ ├── Info.plist │ │ │ └── TodayViewController.swift │ │ └── LICENSE-examples.md │ ├── Examples-macOS/ │ │ ├── IGListKitExamples/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ └── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Base.lproj/ │ │ │ │ └── Main.storyboard │ │ │ ├── Data/ │ │ │ │ └── users.json │ │ │ ├── Helpers/ │ │ │ │ ├── IndexSet+Extensions.swift │ │ │ │ ├── Shuffle.swift │ │ │ │ └── UsersProvider.swift │ │ │ ├── Info.plist │ │ │ ├── Models/ │ │ │ │ └── User.swift │ │ │ ├── View/ │ │ │ │ ├── UserCollectionViewCell.swift │ │ │ │ └── UserCollectionViewCell.xib │ │ │ └── ViewControllers/ │ │ │ └── UsersViewController.swift │ │ ├── IGListKitExamples.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── IGListKitExamples.xcscheme │ │ └── LICENSE-examples.md │ └── Examples-tvOS/ │ ├── IGListKitExamples/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── App Icon & Top Shelf Image.brandassets/ │ │ │ │ ├── App Icon - Large.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── App Icon - Small.imagestack/ │ │ │ │ │ ├── Back.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.imagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.imagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Top Shelf Image Wide.imageset/ │ │ │ │ │ └── Contents.json │ │ │ │ └── Top Shelf Image.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── LaunchImage.launchimage/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── Models/ │ │ │ └── NSObject+IGListDiffable.swift │ │ ├── SectionControllers/ │ │ │ ├── CarouselSectionController.swift │ │ │ ├── DemoSectionController.swift │ │ │ ├── HorizontalSectionController.swift │ │ │ └── LabelSectionController.swift │ │ ├── ViewControllers/ │ │ │ ├── DemosViewController.swift │ │ │ └── NestedAdapterViewController.swift │ │ └── Views/ │ │ ├── CarouselCell.swift │ │ ├── CarouselCell.xib │ │ ├── DemoCell.swift │ │ ├── EmbeddedCollectionViewCell.swift │ │ └── LabelCell.swift │ ├── IGListKitExamples.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── IGListKitExamples.xcscheme │ └── LICENSE-examples.md ├── Gemfile ├── Guides/ │ ├── Best Practices and FAQ.md │ ├── Generating your models using remodel.md │ ├── Getting Started.md │ ├── IGListDiffable and Equality.md │ ├── Installation.md │ ├── Migration.md │ ├── Modeling and Binding.md │ ├── VISION.md │ ├── Working with Core Data.md │ └── Working with UICollectionView.md ├── IGListDiffKit.podspec ├── IGListKit.podspec ├── IGListKit.xcodeproj/ │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ ├── IGListKit-macOS.xcscheme │ ├── IGListKit-tvOS.xcscheme │ └── IGListKit.xcscheme ├── IGListSwiftKit.podspec ├── LICENSE.md ├── Package.swift ├── README.md ├── README.zh.md ├── Source/ │ ├── IGListDiffKit/ │ │ ├── IGListAssert.h │ │ ├── IGListBatchUpdateData.h │ │ ├── IGListBatchUpdateData.mm │ │ ├── IGListCompatibility.h │ │ ├── IGListDiff.h │ │ ├── IGListDiff.mm │ │ ├── IGListDiffKit.h │ │ ├── IGListDiffable.h │ │ ├── IGListExperiments.h │ │ ├── IGListIndexPathResult.h │ │ ├── IGListIndexPathResult.m │ │ ├── IGListIndexSetResult.h │ │ ├── IGListIndexSetResult.m │ │ ├── IGListMacros.h │ │ ├── IGListMoveIndex.h │ │ ├── IGListMoveIndex.m │ │ ├── IGListMoveIndexPath.h │ │ ├── IGListMoveIndexPath.m │ │ ├── Internal/ │ │ │ ├── IGListIndexPathResultInternal.h │ │ │ ├── IGListIndexSetResultInternal.h │ │ │ ├── IGListMoveIndexInternal.h │ │ │ └── IGListMoveIndexPathInternal.h │ │ ├── NSNumber+IGListDiffable.h │ │ ├── NSNumber+IGListDiffable.m │ │ ├── NSString+IGListDiffable.h │ │ └── NSString+IGListDiffable.m │ ├── IGListKit/ │ │ ├── IGListAdapter.h │ │ ├── IGListAdapter.m │ │ ├── IGListAdapterDataSource.h │ │ ├── IGListAdapterDelegate.h │ │ ├── IGListAdapterDelegateAnnouncer.h │ │ ├── IGListAdapterDelegateAnnouncer.m │ │ ├── IGListAdapterMoveDelegate.h │ │ ├── IGListAdapterPerformanceDelegate.h │ │ ├── IGListAdapterUpdateListener.h │ │ ├── IGListAdapterUpdater.h │ │ ├── IGListAdapterUpdater.m │ │ ├── IGListAdapterUpdaterDelegate.h │ │ ├── IGListBatchContext.h │ │ ├── IGListBindable.h │ │ ├── IGListBindingSectionController.h │ │ ├── IGListBindingSectionController.m │ │ ├── IGListBindingSectionControllerDataSource.h │ │ ├── IGListBindingSectionControllerSelectionDelegate.h │ │ ├── IGListBindingSingleSectionController.h │ │ ├── IGListBindingSingleSectionController.m │ │ ├── IGListCollectionContext.h │ │ ├── IGListCollectionScrollingTraits.h │ │ ├── IGListCollectionView.h │ │ ├── IGListCollectionView.m │ │ ├── IGListCollectionViewDelegateLayout.h │ │ ├── IGListCollectionViewLayout.h │ │ ├── IGListCollectionViewLayout.mm │ │ ├── IGListCollectionViewLayoutCompatible.h │ │ ├── IGListCollectionViewLayoutInvalidationContext.h │ │ ├── IGListCollectionViewLayoutInvalidationContext.m │ │ ├── IGListDisplayDelegate.h │ │ ├── IGListGenericSectionController.h │ │ ├── IGListGenericSectionController.m │ │ ├── IGListKit.h │ │ ├── IGListReloadDataUpdater.h │ │ ├── IGListReloadDataUpdater.m │ │ ├── IGListScrollDelegate.h │ │ ├── IGListSectionController.h │ │ ├── IGListSectionController.m │ │ ├── IGListSingleSectionController.h │ │ ├── IGListSingleSectionController.m │ │ ├── IGListSupplementaryViewSource.h │ │ ├── IGListTransitionData.h │ │ ├── IGListTransitionData.m │ │ ├── IGListTransitionDelegate.h │ │ ├── IGListUpdatingDelegate.h │ │ ├── IGListWorkingRangeDelegate.h │ │ ├── Internal/ │ │ │ ├── IGListAdapter+DebugDescription.h │ │ │ ├── IGListAdapter+DebugDescription.m │ │ │ ├── IGListAdapter+UICollectionView.h │ │ │ ├── IGListAdapter+UICollectionView.m │ │ │ ├── IGListAdapterDelegateAnnouncerInternal.h │ │ │ ├── IGListAdapterInternal.h │ │ │ ├── IGListAdapterProxy.h │ │ │ ├── IGListAdapterProxy.m │ │ │ ├── IGListAdapterUpdater+DebugDescription.h │ │ │ ├── IGListAdapterUpdater+DebugDescription.m │ │ │ ├── IGListAdapterUpdaterHelpers.h │ │ │ ├── IGListAdapterUpdaterHelpers.m │ │ │ ├── IGListAdapterUpdaterInternal.h │ │ │ ├── IGListArrayUtilsInternal.h │ │ │ ├── IGListArrayUtilsInternal.m │ │ │ ├── IGListBatchUpdateData+DebugDescription.h │ │ │ ├── IGListBatchUpdateData+DebugDescription.m │ │ │ ├── IGListBatchUpdateState.h │ │ │ ├── IGListBatchUpdateTransaction.h │ │ │ ├── IGListBatchUpdateTransaction.m │ │ │ ├── IGListBindingSectionController+DebugDescription.h │ │ │ ├── IGListBindingSectionController+DebugDescription.m │ │ │ ├── IGListCollectionViewLayoutInternal.h │ │ │ ├── IGListDataSourceChangeTransaction.h │ │ │ ├── IGListDataSourceChangeTransaction.m │ │ │ ├── IGListDebugger.h │ │ │ ├── IGListDebugger.m │ │ │ ├── IGListDebuggingUtilities.h │ │ │ ├── IGListDebuggingUtilities.m │ │ │ ├── IGListDefaultExperiments.h │ │ │ ├── IGListDisplayHandler.h │ │ │ ├── IGListDisplayHandler.m │ │ │ ├── IGListItemUpdatesCollector.h │ │ │ ├── IGListItemUpdatesCollector.m │ │ │ ├── IGListPerformDiff.h │ │ │ ├── IGListPerformDiff.m │ │ │ ├── IGListReloadIndexPath.h │ │ │ ├── IGListReloadIndexPath.m │ │ │ ├── IGListReloadTransaction.h │ │ │ ├── IGListReloadTransaction.m │ │ │ ├── IGListSectionControllerInternal.h │ │ │ ├── IGListSectionMap+DebugDescription.h │ │ │ ├── IGListSectionMap+DebugDescription.m │ │ │ ├── IGListSectionMap.h │ │ │ ├── IGListSectionMap.m │ │ │ ├── IGListUpdateCoalescer.h │ │ │ ├── IGListUpdateCoalescer.m │ │ │ ├── IGListUpdateTransactable.h │ │ │ ├── IGListUpdateTransactionBuilder.h │ │ │ ├── IGListUpdateTransactionBuilder.m │ │ │ ├── IGListViewVisibilityTracker.h │ │ │ ├── IGListViewVisibilityTracker.m │ │ │ ├── IGListViewVisibilityTrackerInternal.h │ │ │ ├── IGListWorkingRangeHandler.h │ │ │ ├── IGListWorkingRangeHandler.mm │ │ │ ├── UICollectionView+DebugDescription.h │ │ │ ├── UICollectionView+DebugDescription.m │ │ │ ├── UICollectionView+IGListBatchUpdateData.h │ │ │ ├── UICollectionView+IGListBatchUpdateData.m │ │ │ ├── UICollectionViewLayout+InteractiveReordering.h │ │ │ ├── UICollectionViewLayout+InteractiveReordering.m │ │ │ ├── UIScrollView+IGListKit.h │ │ │ ├── UIScrollView+IGListKit.m │ │ │ └── UIViewController+IGListAdapterInternal.h │ │ ├── UIViewController+IGListAdapter.h │ │ └── UIViewController+IGListAdapter.m │ ├── IGListSwiftKit/ │ │ ├── IGListAdapter+Async.swift │ │ ├── IGListCollectionContext+Refinements.swift │ │ ├── IGListSingleSectionController+Refinements.swift │ │ ├── IGListSwiftKit.h │ │ ├── ListIdentifiable.swift │ │ └── ListValueSectionController.swift │ └── Info.plist ├── Tests/ │ ├── Assets/ │ │ ├── IGTestNibCell.xib │ │ ├── IGTestNibSupplementaryView.xib │ │ └── IGTestStoryboard.storyboard │ ├── IGListAdapterDelegateAnnouncerTests.m │ ├── IGListAdapterE2ETests.m │ ├── IGListAdapterProxyTests.m │ ├── IGListAdapterStoryboardTests.m │ ├── IGListAdapterTests.m │ ├── IGListAdapterUpdaterTests.m │ ├── IGListBatchUpdateDataTests.m │ ├── IGListBindingSectionControllerTests.m │ ├── IGListBindingSingleSectionControllerTests.m │ ├── IGListCollectionScrollingTraitsTests.m │ ├── IGListCollectionViewLayoutTests.m │ ├── IGListCollectionViewTests.m │ ├── IGListContentInsetTests.m │ ├── IGListDebugDescriptionTests.m │ ├── IGListDebuggerTests.m │ ├── IGListDiffDescriptionStringTests.m │ ├── IGListDiffResultTests.m │ ├── IGListDiffSwiftTests.swift │ ├── IGListDiffTests.h │ ├── IGListDiffTests.m │ ├── IGListDisplayHandlerTests.m │ ├── IGListGenericSectionControllerTests.m │ ├── IGListInteractiveMovingTests.m │ ├── IGListItemUpdatesCollectorTests.m │ ├── IGListKitTests-Bridging-Header.h │ ├── IGListPerformDiffTests.m │ ├── IGListReloadDataUpdaterTests.m │ ├── IGListSectionControllerTests.m │ ├── IGListSectionMapTests.m │ ├── IGListSingleNibItemControllerTests.m │ ├── IGListSingleSectionControllerTests.m │ ├── IGListSingleStoryboardItemControllerTests.m │ ├── IGListTestCase.h │ ├── IGListTestCase.m │ ├── IGListTestHelpers.h │ ├── IGListTransactionTests.m │ ├── IGListUpdateCoalescerTests.m │ ├── IGListViewVisibilityTrackerTests.m │ ├── IGListWorkingRangeHandlerTests.m │ ├── Info.plist │ ├── Objects/ │ │ ├── IGLayoutTestDataSource.h │ │ ├── IGLayoutTestDataSource.m │ │ ├── IGLayoutTestItem.h │ │ ├── IGLayoutTestItem.m │ │ ├── IGLayoutTestSection.h │ │ ├── IGLayoutTestSection.m │ │ ├── IGListAdapterUpdateTester.h │ │ ├── IGListAdapterUpdateTester.m │ │ ├── IGListTestAdapterDataSource.h │ │ ├── IGListTestAdapterDataSource.m │ │ ├── IGListTestAdapterHorizontalDataSource.h │ │ ├── IGListTestAdapterHorizontalDataSource.m │ │ ├── IGListTestAdapterReorderingDataSource.h │ │ ├── IGListTestAdapterReorderingDataSource.m │ │ ├── IGListTestAdapterStoryboardDataSource.h │ │ ├── IGListTestAdapterStoryboardDataSource.m │ │ ├── IGListTestCollectionViewLayout.h │ │ ├── IGListTestCollectionViewLayout.m │ │ ├── IGListTestContainerSizeSection.h │ │ ├── IGListTestContainerSizeSection.m │ │ ├── IGListTestHorizontalSection.h │ │ ├── IGListTestHorizontalSection.m │ │ ├── IGListTestOffsettingLayout.h │ │ ├── IGListTestOffsettingLayout.m │ │ ├── IGListTestSection.h │ │ ├── IGListTestSection.m │ │ ├── IGListTestStoryboardSection.h │ │ ├── IGListTestStoryboardSection.m │ │ ├── IGListTestUICollectionViewDataSource.h │ │ ├── IGListTestUICollectionViewDataSource.m │ │ ├── IGTestBindingSingleItemDataSource.h │ │ ├── IGTestBindingSingleItemDataSource.m │ │ ├── IGTestBindingWithoutDeselectionDelegate.h │ │ ├── IGTestBindingWithoutDeselectionDelegate.m │ │ ├── IGTestCell.h │ │ ├── IGTestCell.m │ │ ├── IGTestDelegateController.h │ │ ├── IGTestDelegateController.m │ │ ├── IGTestDelegateDataSource.h │ │ ├── IGTestDelegateDataSource.m │ │ ├── IGTestDiffingDataSource.h │ │ ├── IGTestDiffingDataSource.m │ │ ├── IGTestDiffingObject.h │ │ ├── IGTestDiffingObject.m │ │ ├── IGTestDiffingSectionController.h │ │ ├── IGTestDiffingSectionController.m │ │ ├── IGTestInvalidateLayoutDataSource.h │ │ ├── IGTestInvalidateLayoutDataSource.m │ │ ├── IGTestInvalidateLayoutObject.h │ │ ├── IGTestInvalidateLayoutObject.m │ │ ├── IGTestInvalidateLayoutSectionController.h │ │ ├── IGTestInvalidateLayoutSectionController.m │ │ ├── IGTestNibSupplementaryView.h │ │ ├── IGTestNibSupplementaryView.m │ │ ├── IGTestNumberBindableCell.h │ │ ├── IGTestNumberBindableCell.m │ │ ├── IGTestObject.h │ │ ├── IGTestObject.m │ │ ├── IGTestReorderableSection.h │ │ ├── IGTestReorderableSection.m │ │ ├── IGTestSingleItemDataSource.h │ │ ├── IGTestSingleItemDataSource.m │ │ ├── IGTestSingleNibItemDataSource.h │ │ ├── IGTestSingleNibItemDataSource.m │ │ ├── IGTestSingleStoryboardItemDataSource.h │ │ ├── IGTestSingleStoryboardItemDataSource.m │ │ ├── IGTestSingleWithoutDeselectionDelegate.h │ │ ├── IGTestSingleWithoutDeselectionDelegate.m │ │ ├── IGTestStoryboardCell.h │ │ ├── IGTestStoryboardCell.m │ │ ├── IGTestStoryboardSupplementarySource.h │ │ ├── IGTestStoryboardSupplementarySource.m │ │ ├── IGTestStoryboardSupplementaryView.h │ │ ├── IGTestStoryboardSupplementaryView.m │ │ ├── IGTestStoryboardViewController.h │ │ ├── IGTestStoryboardViewController.m │ │ ├── IGTestStringBindableCell.h │ │ ├── IGTestStringBindableCell.m │ │ ├── IGTestSupplementarySource.h │ │ └── IGTestSupplementarySource.m │ └── UIViewControllerIGListAdapterTests.m ├── docs/ │ ├── Categories/ │ │ └── UIViewController(IGListAdapter).html │ ├── Categories.html │ ├── Classes/ │ │ ├── IGListAdapter.html │ │ ├── IGListAdapterDelegateAnnouncer.html │ │ ├── IGListAdapterUpdater.html │ │ ├── IGListBatchUpdateData.html │ │ ├── IGListBindingSectionController.html │ │ ├── IGListBindingSingleSectionController.html │ │ ├── IGListCollectionView.html │ │ ├── IGListCollectionViewLayout.html │ │ ├── IGListCollectionViewLayoutInvalidationContext.html │ │ ├── IGListGenericSectionController.html │ │ ├── IGListIndexPathResult.html │ │ ├── IGListIndexSetResult.html │ │ ├── IGListMoveIndex.html │ │ ├── IGListMoveIndexPath.html │ │ ├── IGListSectionController.html │ │ ├── IGListSingleSectionController.html │ │ └── IGListTransitionData.html │ ├── Classes.html │ ├── Constants.html │ ├── Enums/ │ │ ├── IGListAdapterUpdateType.html │ │ ├── IGListDiffOption.html │ │ └── IGListExperiment.html │ ├── Enums.html │ ├── Functions.html │ ├── Guides.html │ ├── Protocols/ │ │ ├── IGListAdapterDataSource.html │ │ ├── IGListAdapterDelegate.html │ │ ├── IGListAdapterMoveDelegate.html │ │ ├── IGListAdapterPerformanceDelegate.html │ │ ├── IGListAdapterUpdateListener.html │ │ ├── IGListAdapterUpdaterDelegate.html │ │ ├── IGListBatchContext.html │ │ ├── IGListBindable.html │ │ ├── IGListBindingSectionControllerDataSource.html │ │ ├── IGListBindingSectionControllerSelectionDelegate.html │ │ ├── IGListCollectionContext.html │ │ ├── IGListCollectionViewDelegateLayout.html │ │ ├── IGListCollectionViewLayoutCompatible.html │ │ ├── IGListDiffable.html │ │ ├── IGListDisplayDelegate.html │ │ ├── IGListScrollDelegate.html │ │ ├── IGListSingleSectionControllerDelegate.html │ │ ├── IGListSupplementaryViewSource.html │ │ ├── IGListTransitionDelegate.html │ │ ├── IGListUpdatingDelegate.html │ │ └── IGListWorkingRangeDelegate.html │ ├── Protocols.html │ ├── Structs/ │ │ ├── IGListAdaptiveCoalescingExperimentConfig.html │ │ ├── IGListAdaptiveDiffingExperimentConfig.html │ │ └── IGListCollectionScrollingTraits.html │ ├── Structs.html │ ├── Type Definitions/ │ │ ├── IGListAdaptiveCoalescingExperimentConfig/ │ │ │ └── IGListAdaptiveCoalescingExperimentConfig.html │ │ ├── IGListAdaptiveCoalescingExperimentConfig.html │ │ ├── IGListAdaptiveDiffingExperimentConfig/ │ │ │ └── IGListAdaptiveDiffingExperimentConfig.html │ │ ├── IGListAdaptiveDiffingExperimentConfig.html │ │ ├── IGListCollectionScrollingTraits/ │ │ │ └── IGListCollectionScrollingTraits.html │ │ └── IGListCollectionScrollingTraits.html │ ├── Type Definitions.html │ ├── best-practices-and-faq.html │ ├── css/ │ │ ├── highlight.css │ │ └── jazzy.css │ ├── generating-your-models-using-remodel.html │ ├── getting-started.html │ ├── iglistdiffable-and-equality.html │ ├── index.html │ ├── installation.html │ ├── js/ │ │ ├── jazzy.js │ │ ├── jazzy.search.js │ │ └── typeahead.jquery.js │ ├── migration.html │ ├── modeling-and-binding.html │ ├── search.json │ ├── undocumented.json │ ├── vision.html │ ├── working-with-core-data.html │ └── working-with-uicollectionview.html ├── remodel-plugin/ │ ├── features/ │ │ └── iglistdiffable.feature │ └── src/ │ ├── __tests__/ │ │ └── plugins/ │ │ └── iglistdiffable-test.ts │ └── plugins/ │ ├── iglistdiffable-utils.ts │ └── iglistdiffable.ts └── scripts/ ├── build_docs.sh ├── generate_spm_sources_layout.sh ├── lint.sh └── version.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to IGListKit We want to make contributing to this project as easy and transparent as possible, and actively welcome your pull requests. If you run into problems, please open an issue on GitHub. ## Pull Requests 1. Fork the repo and create your branch from `main`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes. 5. Make sure your code lints. 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 7. Add an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. ## Experimental changes If your change can't be unit tested, we might ask that you add your change as an experiment so that we can verify your change works. To do this, first add a new option to [IGListExperiment](https://github.com/Instagram/IGListKit/blob/main/Source/Common/IGListExperiments.h#L17). Then, use an `experiments` bitmask wherever your change is and wrap it in a check to see if it is enabled: ```swift IGListExperimentEnabled(self.experiments, IGListExperimentMyAwesomeChange) { // your code here } ``` Once your experiment is confirmed we will remove the option and wrapping check! ## How we do major and minor releases Everything merges into `main`. When we cut a release, we merge from `main` into `stable`, tag, and push to CocoaPods. *Example:* If current release is `2.1.0`, then any commits for `2.2.0` go into `stable` while commits for `3.0` would go to `main`. ## Testing Keep in mind that we want 99% test coverage at all times. If you add new code, please make sure it gets tested! When fixing bugs, try to reproduce the bug in a unit test and then fix the test. This makes sure we never regress that issue again. ## Contributor License Agreement ("CLA") In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects. Complete your CLA here: ## Issues We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue. ## Coding Style * 4 spaces for indentation rather than tabs * Public classes and methods must contain header documentation * Use plain C functions instead of class methods whenever possible * Restrict subclassing (`IGLK_SUBCLASSING_RESTRICTED` macro or `final` in Swift), unless the class is designed for subclassing. * Use instance variables instead of properties * Create local variables instead of repeatedly accessing properties like `self.property`. This results in a larger binary and extra `objc_msgSend(...)` calls. ## Updating Testing Dependencies If you need a different version of one of the testing dependencies, you will need to first [install Cocoapods](https://guides.cocoapods.org/using/getting-started.html): ``` $ [sudo] gem install cocoapods ``` Then within the project directory, run `pod install` to update the dependency to that version. ## License By contributing to `IGListKit`, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## New issue checklist - [ ] I have reviewed the [`README`](https://github.com/Instagram/IGListKit/blob/main/README.md) and [documentation](http://instagram.github.io/IGListKit) - [ ] I have searched [existing issues](https://github.com/Instagram/IGListKit/issues) and this is not a duplicate - [ ] I have attempted to reproduce the issue and include an example project. ### General information - `IGListKit` version: - iOS version(s): - CocoaPods/Carthage version: - Xcode version: - Devices/Simulators affected: - Reproducible in the demo project? (Yes/No): - Related issues: ### Debug information ```bash # Please include debug logs using the following lldb command: po [IGListDebugger dump] ``` ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Changes in this pull request Issue fixed: # ### Checklist - [ ] All tests pass. Demo project builds and runs. - [ ] I added tests, an experiment, or detailed why my change isn't tested. - [ ] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [ ] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/main/.github/CONTRIBUTING.md) ================================================ FILE: .github/RELEASE_CHECKLIST.md ================================================ # Release Checklist Here are the steps for creating and publishing a new release for `IGListKit`. - Final review and update of header docs and guides - Final review of changelog - Regenerate docs - Update pod spec version - Update xcodeproj version - Run `pod install` on all examples (**must happen on FB internal** because of sync issues) - Merge `main` into `stable` via cmd-line and push - Confirm `stable` is `0|0` [ahead/behind](https://github.com/Instagram/IGListKit/branches) - Create [GitHub release](https://github.com/Instagram/IGListKit/releases) from `stable` - Paste changelog into GH release notes - Publish GitHub release - Run `pod lib lint` - Push updated podspec: `pod trunk push IGListKit.podspec` - Verify new release on [CocoaPods](https://cocoapods.org/pods/IGListKit) - Tweet all the tweets ================================================ FILE: .github/workflows/CI.yml ================================================ name: "IGListKit CI" on: push: branches: - main pull_request: branches: - '*' jobs: macOS: name: Unit Test macOS runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app PROJECT_NAME: IGListKit.xcodeproj SCHEME_NAME: IGListKit-macOS steps: - name: Checkout uses: actions/checkout@v3 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: Run unit tests for macOS run: | set -o pipefail xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "platform=macOS" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c iOS: name: Unit Test iOS runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app PROJECT_NAME: IGListKit.xcodeproj SCHEME_NAME: IGListKit strategy: matrix: destination: ["platform=iOS Simulator,name=iPhone 16 Pro Max,OS=18.2"] steps: - name: Checkout uses: actions/checkout@v3 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: iOS - ${{ matrix.destination }} run: | set -o pipefail xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c - name: Upload code coverage run: bundle exec slather env: COVERAGE_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} CI_PULL_REQUEST: ${{ github.event.number }} GIT_BRANCH: ${{ github.head_ref || github.ref_name }} tvOS: name: Unit Test tvOS runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app PROJECT_NAME: IGListKit.xcodeproj SCHEME_NAME: IGListKit-tvOS strategy: matrix: destination: ["platform=tvOS Simulator,name=Apple TV,OS=18.2"] steps: - name: Checkout uses: actions/checkout@v3 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: Run unit tests for tvOS run: | set -o pipefail xcodebuild build build-for-testing -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c xcodebuild analyze test-without-building -project "${{ env.PROJECT_NAME }}" -scheme "${{ env.SCHEME_NAME }}" -destination "${{ matrix.destination }}" -configuration Debug CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=YES | bundle exec xcpretty -c CocoaPods: name: CocoaPods Lint runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app steps: - name: Checkout uses: actions/checkout@v3 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: Run IGListDiffKit podspec lint run: bundle exec pod lib lint IGListDiffKit.podspec --allow-warnings - name: Run IGListKit podspec lint run: bundle exec pod lib lint IGListKit.podspec --allow-warnings "--include-podspecs=IGListDiffKit.podspec" - name: Run IGListSwiftKit podspec lint run: bundle exec pod lib lint IGListSwiftKit.podspec --allow-warnings "--include-podspecs=*.podspec" SPM-layout-generator: name: Verify generate_spm_sources_layout.sh is not broken runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app PROJECT_NAME: IGListKit.xcodeproj IOS_DESTINATION: "platform=iOS Simulator,name=iPhone 16 Pro Max,OS=18.2" SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH: "spm/Sources/IGListDiffKit/include" SPM_IG_LIST_DIFF_KIT_SOURCES_PATH: "spm/Sources/IGListDiffKit" SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH: "spm/Sources/IGListKit/include" SPM_IG_LIST_KIT_SOURCES_PATH: "spm/Sources/IGListKit" steps: - name: Checkout uses: actions/checkout@v3 - name: Manually clean SPM Sources folder run: | rm -rf "${{ env.SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH }}/*.*" rm -rf "${{ env.SPM_IG_LIST_DIFF_KIT_SOURCES_PATH }}/*.*" rm -rf "${{ env.SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH }}/*.*" rm -rf "${{ env.SPM_IG_LIST_KIT_SOURCES_PATH }}/*.*" - name: Regenerate SPM layout run: sh scripts/generate_spm_sources_layout.sh - name: Clean project's ${{ env.PROJECT_NAME }} run: | rm -rf "${{ env.PROJECT_NAME }}" - name: Verify IGListKit can be build using Package.swift which was generated by 'scripts/generate_spm_sources_layout.sh' run: xcodebuild -scheme "IGListKit" build -destination "${{ env.IOS_DESTINATION }}" | xcpretty SPM-build-from-Package: name: Verify SPM build by invoking `xcodebuild` on Package.swift runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app PROJECT_NAME: IGListKit.xcodeproj IOS_DESTINATION: "platform=iOS Simulator,name=iPhone 16 Pro Max,OS=18.2" strategy: matrix: schemeName: ["IGListDiffKit", "IGListKit", "IGListSwiftKit"] steps: - name: Checkout uses: actions/checkout@v3 - name: Clean project's ${{ env.PROJECT_NAME }} run: | rm -rf "${{ env.PROJECT_NAME }}" - name: Run ${{ matrix.schemeName}} using Package.swift run: xcodebuild -scheme "${{ matrix.schemeName}}" build -destination "${{ env.IOS_DESTINATION }}" | xcpretty Carthage-XCFramework: name: Verify Carthage build XCFramework runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app steps: - name: Checkout uses: actions/checkout@v3 - name: Carthage build framework run: carthage build --no-skip-current --use-xcframeworks Build-Examples: name: Build Examples and UI tests. runs-on: macos-14 env: DEVELOPER_DIR: /Applications/Xcode.app IOS_EXAMPLE_XCODEPROJ: Examples/Examples-iOS/IGListKitExamples.xcodeproj TVOS_EXAMPLE_XCODEPROJ: Examples/Examples-tvOS/IGListKitExamples.xcodeproj MACOS_EXAMPLE_XCODEPROJ: Examples/Examples-macOS/IGListKitExamples.xcodeproj EXAMPLE_SCHEME: IGListKitExamples strategy: matrix: iosDestination: ["platform=iOS Simulator,name=iPhone 16 Pro Max,OS=18.2"] tvOSDestination: ["platform=tvOS Simulator,name=Apple TV 4K (3rd generation)"] macOSDestination: ["platform=macOS"] macCatalystDestination: ["platform=macOS,variant=Mac Catalyst"] steps: - name: Checkout uses: actions/checkout@v3 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: Preboot iOS Simulator uses: futureware-tech/simulator-action@v2 with: model: 'iPhone 16 Pro Max' os_version: '=18.2' - name: Build iOS Example - ${{ matrix.iosDestination }} run: | set -o pipefail xcodebuild build build-for-testing -project "${{ env.IOS_EXAMPLE_XCODEPROJ }}" -scheme "${{ env.EXAMPLE_SCHEME }}" -destination "${{ matrix.iosDestination }}" -configuration Debug ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c - name: Run iOS Example's UI Tests - ${{ matrix.iosDestination }} run: | set -o pipefail xcodebuild build test -project "${{ env.IOS_EXAMPLE_XCODEPROJ }}" -scheme "${{ env.EXAMPLE_SCHEME }}" -destination "${{ matrix.iosDestination }}" -configuration Debug ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c - name: Build tvOS Example - ${{ matrix.tvOSDestination }} run: | set -o pipefail xcodebuild build -project "${{ env.TVOS_EXAMPLE_XCODEPROJ }}" -scheme "${{ env.EXAMPLE_SCHEME }}" -destination "${{ matrix.tvOSDestination }}" -configuration Debug ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c - name: Build macOS Example - ${{ matrix.macOSDestination }} run: | set -o pipefail xcodebuild build -project "${{ env.MACOS_EXAMPLE_XCODEPROJ }}" -scheme "${{ env.EXAMPLE_SCHEME }}" -destination "${{ matrix.macOSDestination }}" -configuration Debug ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c - name: Build Mac Catalyst Example - ${{ matrix.macCatalystDestination }} run: | set -o pipefail xcodebuild build -project "${{ env.IOS_EXAMPLE_XCODEPROJ }}" -scheme "${{ env.EXAMPLE_SCHEME }}" -destination "${{ matrix.macCatalystDestination }}" -configuration Debug ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO | bundle exec xcpretty -c Danger: name: Run Danger if: github.event_name == 'pull_request' runs-on: macos-14 env: DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Cache RubyGems uses: actions/cache@v3 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | ${{ runner.os }}-gems- - name: Install ruby gems. run: bundle install - name: Run Danger run: bundle exec danger --verbose ================================================ FILE: .gitignore ================================================ .DS_Store Podfile.lock Gemfile.lock # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## Build generated build/ DerivedData/ ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xcuserstate ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM ## Docs docs/docsets/ ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control Pods/ # Add this line if you want to avoid checking in source code from the Xcode workspace *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build # Bundler .bundle vendor # Jetbrains .idea ================================================ FILE: .slather.yml ================================================ ci_service: github coverage_service: coveralls xcodeproj: IGListKit.xcodeproj workspace: IGListKit.xcworkspace scheme: IGListKit source_directory: Source ignore: - ../**/*/Xcode* - Tests/* ================================================ FILE: .swiftlint.yml ================================================ included: - Examples - Source/IGListSwiftKit excluded: - Pods opt_in_rules: - empty_count - number_separator - operator_usage_whitespace - sorted_imports - vertical_parameter_alignment_on_call - overridden_super_call disabled_rules: - force_unwrapping - comma - line_length force_cast: warning force_try: warning weak_delegate: error type_body_length: warning: 300 error: 400 file_length: warning: 500 error: 1200 type_name: min_length: 4 max_length: warning: 40 error: 50 excluded: iPhone identifier_name: min_length: error: 4 excluded: - id - url - URL - pk - day - map - row - nib - GlobalAPIKey - to - obj - str - set reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) ================================================ FILE: CHANGELOG.md ================================================ # CHANGELOG The changelog for `IGListKit`. Also see the [releases](https://github.com/instagram/IGListKit/releases) on GitHub. 5.3.0 (Upcoming Release) ----- 5.2.0 ----- ### Enhancements - Added a more defensive check for NaN values in item bounds when applying layout attributes in `-[IGListCollectionViewLayout layoutAttributesForItemAtIndexPath:]`. Minqi Ma [(358fe894)](https://github.com/Instagram/IGListKit/commit/358fe894f2c66a14ac1eda7ba09a2de158373cd5) - Improved assertion details in `IGListAdapter+UICollectionView.m`. [Maxime Ollivier](https://github.com/maxolls) [(7240131c)](https://github.com/Instagram/IGListKit/commit/7240131ceaa7784bfed60fdb9a623e3b7f4cdf5b) - Formalized assertions that `IGListAdapterDelegateAnnouncer` is main-thread confined. Andrew Cuneo [(7dddb0d4)](https://github.com/Instagram/IGListKit/commit/7dddb0d4c83c14a3cade15a24a9df5b61f7a6836) ### Fixes - An `EXC_BAD_ACCESS` crash in `IGListSectionMap.m` during `IGListAdapter` deallocation. [Hoà V. DINH](https://github.com/dinhvh) [(d1ddab8e)](https://github.com/Instagram/IGListKit/commit/d1ddab8e20fe8b3effc70dc878697e0f421f9a25) - A crash where an index value provided to `-[IGListBindingSectionController sizeForItemAtIndex:]` was out of bounds. Kaur Ishnoor [(0a0b11bd)](https://github.com/Instagram/IGListKit/commit/0a0b11bdc81d96aa6525d558715bc46d91393711) 5.1.0 ----- ### Breaking Changes - Added methods to the `IGListAdapterDelegate` to include callback methods for any time a cell will appear or did disappear from the screen. This can be used in conjunction with the `IGListAdapterDelegateAnnouncer` to listen globally for cell lifecycle events. Any implementer of `IGListAdapterDelegate` must update to include these methods. [Jesse Seidman](https://github.com/jseidman95) [(fa50aa1c)](https://github.com/Instagram/IGListKit/commit/fa50aa1cf1be85940787e211d23eb5e7873dbf24), [(d1d6f9d5)](https://github.com/Instagram/IGListKit/commit/d1d6f9d52caef89efd11444161ac341266d36ccf), [(03049f74)](https://github.com/Instagram/IGListKit/commit/03049f742f46a6ac28abedd249d176cac4694990) - Added `willCrash` delegate methods in order to capture incoming crashes in IGListKit for analysis. Any implementer of `IGListAdapterUpdaterDelegate` and/or `IGListUpdatingDelegate` must update to include these methods. [Hoà V. DINH](https://github.com/dinhvh) [(da1050ef)](https://github.com/Instagram/IGListKit/commit/da1050ef397c9cf38e88ac047196fe53fe6febb0) ### Enhancements - Replaced CocoaPods with SPM as the new dependency manager of `IGListKit.xcodeproj` when running unit tests. [Tim Oliver](https://github.com/timoliver) [(2964f066)](https://github.com/Instagram/IGListKit/commit/2964f066a3b0ad533f2eeda71096caeb0b54500a) - Added handling for new collection view cell dequeue behavior in iOS 18 [Maxime Ollivier](https://github.com/maxolls) [(4bad7d52)](https://github.com/Instagram/IGListKit/commit/4bad7d52604c695e48c0c161fed631aad8511449) - Added support for Swift Concurrency [Mark Davis](https://instagram.com/m4rk) [(0ec2d52)](https://github.com/Instagram/IGListKit/commit/0ec2d526f35374dc0366024f8bb62aac4db1fd8d) - Exposed an option to disable layout cache invalidation when new items are inserted [Constantine Fry](https://github.com/constantine-fry) [(700905e)](https://github.com/Instagram/IGListKit/commit/700905e628bf56abebce5bbdb0698ecade3c9842) - Exposed `IGListCollectionViewLayoutInvalidationContext` as a public interface [(1560956)](https://github.com/Instagram/IGListKit/commit/1560956da3ce27e1816c641541dbdc616cf5ef56) - Annotated main API classes with `NS_SWIFT_UI_ACTOR` in preparation of moving to Swift 6 [Cory Wilhite](https://github.com/corywilhite) [(a28d835)](https://github.com/Instagram/IGListKit/commit/a28d835b739af5136841622854b276d07cda7852) - Added `autoDeselectEnabled` on `IGListAdapter` to free each section-controller from having to do this and avoid bugs [Maxime Ollivier](https://github.com/maxolls) [(fec6b0e)](https://github.com/Instagram/IGListKit/commit/fec6b0ef0f4a0224519b868cb51600ad3fdf6923) - Added `associatedListAdapters` as a property of `UIViewController` to track `IGListAdapter` instances bound to that view controller. [Maxime Ollivier](https://github.com/maxolls) [(9c0ef52)](https://github.com/Instagram/IGListKit/commit/9c0ef5271583fadaa10fda6340794c20bcf609b2) - Added assert when a section's `diffIdentifier` changed before an update starts, which could manifest in a few different crashes [Maxime Ollivier](https://github.com/maxolls) [(64ba471)](https://github.com/Instagram/IGListKit/commit/64ba4712012f074acbafa3fee05aefb28fa06fd2) - Added support for iOS 13 Context Menus with `contextMenuConfigurationForItemAt` method. [Jérôme B.](https://github.com/jeromeboursier) [(23daf6d)](https://github.com/Instagram/IGListKit/commit/23daf6de7290198b775062d04906ba3019519ff0). ### Fixes - A crash that could periodically occur when collection view layout attributes were `nil` [(6e91299)](https://github.com/Instagram/IGListKit/commit/6e9129978e0e3537aec53eea2d6909e45fe057f9) - A crash that could occur when reloading the same cell index multiple times [Maxime Ollivier](https://github.com/maxolls) [(0c25779)](https://github.com/Instagram/IGListKit/commit/0c257795e4a6e448fb76b5f15ebc83a1afcbd272) - A crash in `IGListCollectionViewLayout` that could occur if a nil `indexPath` value was provided [(5ee2207)](https://github.com/Instagram/IGListKit/commit/5ee22079822ef30d05366d58173b09e238b6e44b) 5.0.0 ----- ### Breaking Changes - Changed iOS deployment target to 11.0 and macOS deployment target to 10.13 [Kent Sutherland](https://github.com/ksuther) [(#1573)](https://github.com/Instagram/IGListKit/pull/1573) - Removed unneeded diffing functions `IGListDiffExperiment(...)` and `IGListDiffPathsExperiment(...)`. [Maxime Ollivier](https://github.com/maxolls) [(254c041)](https://github.com/Instagram/IGListKit/commit/254c04196a6b906a155d8a1dd670c720500bed6c) - `ListSectionController.collectionContext` and `ListGenericSectionController.object` are now implicitly-unwrapped optionals in Swift. [Nate Stedman](https://github.com/natestedman) [(a6526ce)](https://github.com/Instagram/IGListKit/commit/a6526ce097fe38de85459cd6a34d948ec8440db7) - The argument of `IGListGenericSectionController`'s `-didUpdateToObject:` is now generic, not `id`. [Nate Stedman](https://github.com/natestedman) [(a6526ce)](https://github.com/Instagram/IGListKit/commit/a6526ce097fe38de85459cd6a34d948ec8440db7) - Updated `IGListUpdatingDelegate`, including a new method to safely perform `[IGListAdapter setDataSource:]` and changes to `-performUpdateWithCollectionViewBlock` that allows section-controllers to be created before the diffing (and therefore use a more accurate `toObjects` array) [Maxime Ollivier](https://github.com/maxolls) [(43af883)](https://github.com/Instagram/IGListKit/commit/43af8838dfdcfc50b8145c56cfecb5f5ed9195a5) ```objc // OLD - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock fromObjects:(nullable NSArray> *)fromObjects toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock animated:(BOOL)animated objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock completion:(nullable IGListUpdatingCompletion)completion; // NEW - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(nullable IGListUpdatingCompletion)completion; // NEW - (void)performDataSourceChange:(IGListDataSourceChangeBlock)block; ``` - Removed `allowsBackgroundReloading` from `IGListAdapterUpdater` because it's causing performance issues and other bugs. [Maxime Ollivier](https://github.com/maxolls) [(032e1b0)](https://github.com/Instagram/IGListKit/commit/032e1b0b8367e68ef3015f0dc7dfe2f3ff2bae0c) - Introducing `allowsBackgroundDiffing` on `IGListAdapterUpdater`! This property lets the updater perform the diffing on a background thread. Originally introduced by Ryan Nystrom a while back. [Maxime Ollivier](https://github.com/maxolls) [(9a11f6)](https://github.com/Instagram/IGListKit/commit/9a11f6b55f02a8a89494035fb17203655e454404) - Updated `scrollToObject:` method in `IGListAdapter` to include a new parameter `additionalOffset` to handle shifting the final scroll position by some vertical or horizontal offset depending on the scroll direction. This allows the object to be shown at the correct position when it is scrolled to in a list with sticky headers. [Anna Tang](https://www.instagram.com/anna.likesfood/) [(f2166c3)](https://github.com/Instagram/IGListKit/commit/f2166c358b3ed0fb3e9dd55d2bed9b7fe7a6a05e) ```objc // OLD - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection scrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; // NEW - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection scrollPosition:(UICollectionViewScrollPosition)scrollPosition additionalOffset:(CGPoint)additionalOffset animated:(BOOL)animated; ``` - Unshipped the `IGListExperimentSkipViewSectionControllerMap` experiment as it was no longer being used. [Maxime Ollivier](https://github.com/maxolls) [(99e24af)](https://github.com/Instagram/IGListKit/commit/99e24afbf872c0d858291084cfa45243f0c9a448) - Unshipped the `IGListExperimentSkipPerformUpdateIfPossible` experiment since it wasn't considered safe enough. [Maxime Ollivier](https://github.com/maxolls) [(b3a22ad)](https://github.com/Instagram/IGListKit/commit/b3a22ad554995b3a869bb1b75369108e65bd867e) ### Enhancements - Added `traitCollection` property to `IGListAdapter` in order to access the current trait collection of the underlying collection view. [Sash Zats](https://github.com/zats) [(53a96a9)](https://github.com/Instagram/IGListKit/commit/53a96a9896732ffeb2683953aa6bd23642743ec8) - Added `viewForSupplementaryElementOfKind:atIndex:sectionController:` to `IGListAdapter`. [ryanmathews](https://www.instagram.com/_rmathews_/) [(c708a10)](https://github.com/Instagram/IGListKit/commit/c708a10757944f147110a31be661d015e9fc901c) - Replaced usage of `method_exchangeImplementations` with `class_replaceMethod` for increased performance. [Saagar Jha](https://github.com/saagarjha) [(#1583)](https://github.com/Instagram/IGListKit/pull/1583) - Added `shouldDeselectItemAtIndex:` to `IGListSectionController`. [bladeofky](https://github.com/bladeofky) [(b22a10e)](https://github.com/Instagram/IGListKit/commit/b22a10e47ffa87c79993ea19db7b52605e83ebbf) - Added `shouldSelectItemAtIndex:` to `IGListSectionController`. [dirtmelon](https://github.com/dirtmelon) [(#1479)](https://github.com/Instagram/IGListKit/pull/1479) - Added [Mac Catalyst](https://developer.apple.com/mac-catalyst/) support. [Petro Rovenskyy](https://github.com/3a4oT/) [(#1487)](https://github.com/Instagram/IGListKit/pull/1487) - Introduce `IGListSwiftKit`, with Swift refinements for `dequeueReusableCellOfClass` methods. [Koen Punt](https://github.com/koenpunt) [(#1388)](https://github.com/Instagram/IGListKit/pull/1388). - Added `APPLICATION_EXTENSION_API_ONLY` support for `IGListDiffKit` [Peter Meyers](https://github.com/pm-dev) [(#1422)](https://github.com/Instagram/IGListKit/pull/1422) - Improved performance by deferring requesting objects from the `IGListAdapterDataSource` until just before diffing is executed. If n updates are coalesced into one, this results in just a single request for objects from the data source. Shipped with experiment `IGListExperimentDeferredToObjectCreation` from Ryan Nystrom. [Maxime Ollivier](https://github.com/maxolls) [(7fc4384)](https://github.com/Instagram/IGListKit/commit/7fc4384a786968027f529ad24b8d821f671d25af) - Improved performance by using `reloadData` when there are too many diffing updates. Shipped with experiment `IGListExperimentReloadDataFallback` from Ryan Nystrom. [Maxime Ollivier](https://github.com/maxolls) [(86ecc60)](https://github.com/Instagram/IGListKit/commit/86ecc600856b4199f8a31359140753955f02b5a4) - Small performance improvement by replacing `NSSet` with `NSArray` during the data update to avoid unnecessary hashing, especially when dealing with lots of large objects with non trivial hashes. [Maxime Ollivier](https://github.com/maxolls) [(c0cf10d)](https://github.com/Instagram/IGListKit/commit/c0cf10d84c0d41b5c02af3a9f3268e438b8e1e77) - Lazy initialize the `-emptyViewForListAdapter:` [Maxime Ollivier](https://github.com/maxolls) [(29d4640)](https://github.com/Instagram/IGListKit/commit/29d464099782813113246da47cb9c7bf4676a311) - Updated `IGListAdapterUpdater` to be safer, more performant, and better organized! [Maxime Ollivier](https://github.com/maxolls) [(247e7ca)](https://github.com/Instagram/IGListKit/commit/247e7cac6547d52dbabf0c6fdcb7fa38b00d0e0f) - Safely handles `[IGListAdapter setDataSource:]` by also invalidating the `UICollectionView` data. - Safely handles `[IGListAdapter setCollectionView:]` by cancelling on-going transactions. - Safely handles returning nil `IGListSectionController` from `IGListAdapterDataSource` by dumping objects that don't have a controller before the diffing. - Checks that the `UICollectionView` section count matches the `IGListAdapter` before committing the update, otherwise fallback to a reload. - Schedules an update block (`dispatch_async`) only when needed, instead of scheduling on every single call to `-performUpdateWithCollectionViewBlock`. - Wraps each update in a `transaction` that can be easily cancelled. - Uses methods instead of blocks to make the callstack easier to read in crash reports. - Unblocks `IGListExperimentBackgroundDiffing` - The `IGListExperimentFixCrashOnReloadObjects` experiment succeeded and has officially been implemented into IGListKit. [Maxime Ollivier](https://github.com/maxolls) [(99e24af)](https://github.com/Instagram/IGListKit/commit/99e24afbf872c0d858291084cfa45243f0c9a448) - Added `IGListExperimentKeepPointerToCollectionViewDataSource` experiment as a potential solution for certain crashes being periodically observed. - Added `IGListExperimentDisableAnimationOnUpdates` experiment to optionally disable update animations in `IGListAdapter` instances. [(eeb5208)](https://github.com/Instagram/IGListKit/commit/eeb5208911fe340b39d2cc3231d1cd59df16e215) ### Fixes - Don't crash if you use `IGListSectionController` without a subclass [Maxime Ollivier](https://github.com/maxolls) [(6ea2b91)](https://github.com/Instagram/IGListKit/commit/6ea2b91150040911bcbe4f98201e36035e26e47f) - Testing crash fix when calling `-[IGListAdapter reloadObjects ...]` during an update [Maxime Ollivier](https://github.com/maxolls) [(cd3f84f)](https://github.com/Instagram/IGListKit/commit/cd3f84f22709dc6bd0153476372cade8167b9513) - Repaired Swift Package Manager support. [Petro Rovenskyy](https://github.com/3a4oT/) [(#1487)](https://github.com/Instagram/IGListKit/pull/1487) - `IGListCollectionViewLayout` should get the section/index counts via `UICollectionView` to stay in sync, instead of the `dataSource` [Maxime Ollivier](https://github.com/maxolls) [(677ce77)](https://github.com/Instagram/IGListKit/commit/677ce77ecad11850f61436681ee1d04a5e67e96a) - Remove `[collectionView layoutIfNeeded]` before scrolling in `[IGListAdapter scrollToObject...]` to avoid creating off-screen cells. [Maxime Ollivier](https://github.com/maxolls) [(ea03bc9)](https://github.com/Instagram/IGListKit/commit/ea03bc959dcfdc5937655ca471135f874980f0ad) - Remove `[collectionView layoutIfNeeded]` before updating in `[IGListAdapterUpdater performBatchUpdates...]` to fix occasional glitches. [Maxime Ollivier](https://github.com/maxolls) [(aca18c7)](https://github.com/Instagram/IGListKit/commit/aca18c747009c5d2c3825cf0af1ea1d214afba0c) - Fixed `IGListAdapterUpdaterDelegate` by 1) calling `willReloadDataWithCollectionView` on fallback reloads and 2) making sure `willPerformBatchUpdatesWithCollectionView` is only called when performing a batch update. [Maxime Ollivier](https://github.com/maxolls) [(29bf582)](https://github.com/Instagram/IGListKit/commit/29bf582f479ddf6beb118abc83ba7a8ea87543b0) - Fixed missing update when calling `performUpdatesAnimated` multiple times quickly and using the `reloadDataFallback()`. [Maxime Ollivier](https://github.com/maxolls) [(a70d2d7)](https://github.com/Instagram/IGListKit/commit/a70d2d70ae8975af5b00f93ae596589b42017c38) - Request the `UICollectionView` until just-before we update. This way if the `UICollectionView` is changed between update-queue and execution, we guarantee the update is performed on the correct view. Ship with experiment `IGListExperimentGetCollectionViewAtUpdate` from Ryan Nystrom. [Maxime Ollivier](https://github.com/maxolls) [(34c935c)](https://github.com/Instagram/IGListKit/commit/34c935c1a5fee83283beece8f1c6b3d6344f275d) - Fixed unsigned integer overflow handling in `IGListBatchUpdateData` [Jason Hsu](https://github.com/tuoxie007) [(#1299)](https://github.com/Instagram/IGListKit/pull/1299) - Fixed when collection views wouldn't recalculate its layout when its bound changes. [Sash Zats](https://github.com/zats) [(d220f8a)](https://github.com/Instagram/IGListKit/commit/d220f8a73fa91fb8444398992b2667f24a38b7a7) - Fixed when calling `invalidateLayout` on `IGListCollectionViewLayout` wouldn't perform layout recalculation. [Tim Oliver](https://github.com/timoliver) [(ffd51e6)](https://github.com/Instagram/IGListKit/commit/ffd51e6235c761b14c52ac657c69dd52ee7b321f) 4.0.0 ----- ### Breaking Changes - Added Swift annotation name to `IGListAdapterDelegate` which removes `IG` prefix. The new name for Swift clients is `ListAdapterDelegate`. [Andrea Antonioni](https://github.com/andreaantonioni) [(#1116)](https://github.com/Instagram/IGListKit/pull/1116) - Remove support for iOS 8 [Ian Perry](https://github.com/iperry90) [(#1381)](https://github.com/Instagram/IGListKit/pull/1381) - `IGListKit` has been split into `IGListKit` and `IGListDiffKit` for Xcode and Carthage builds. Cocoapods continues to use an all-inclusive `IGListKit` podspec. [Nate Stedman](https://github.com/natestedman) [(#1377)](https://github.com/Instagram/IGListKit/pull/1377) - Remove `coalescanceTime` from IGListAdapterUpdate, since it increase crash rate. [Zhisheng Huang](https://github.com/lorixx) [(2f76e8c)](https://github.com/Instagram/IGListKit/commit/2f76e8ce684bf7cea75ee52f25d4ea0af3e0081b) - All `IGListBindingSectionControllerSelectionDelegate` methods are now required. [Bofei Zhu](https://github.com/zhubofei) [(#1186)](https://github.com/Instagram/IGListKit/pull/1186) - Renamed `[IGListAdapterUpdatingDelegate listAdapterUpdater:willPerformBatchUpdatesWithCollectionView:]` to `[IGListAdapterUpdatingDelegate listAdapterUpdater:willPerformBatchUpdatesWithCollectionView:fromObjects:toObjects:listIndexSetResult:]` to include more supporting info on updated objects. [Jeremy Cohen](https://github.com/jeremycohen) [(b200dda)](https://github.com/Instagram/IGListKit/commit/b200ddacf59547641d77e31d313566c78944a67b) - Renamed `[IGListAdapterUpdatingDelegatelistAdapterUpdater:collectionView:willCrashWithException:fromObjects:toObjects:updates:]` to `[ IGListAdapterUpdatingDelegatelistAdapterUpdater:collectionView:willCrashWithException:fromObjects:toObjects:diffResult:updates:]` to include diff result info. [Zhisheng Huang](https://github.com/lorixx) [(039e77e)](https://github.com/Instagram/IGListKit/commit/039e77e3593251c8711497f76ab25857d641ecee) - Remove `IGListStackedSectionController`. [Hanton Yang](https://github.com/hanton) [(#1355)](https://github.com/Instagram/IGListKit/pull/1355) ### Enhancements - Added `IGListCollectionScrollingTraits` for exposing `UICollectionView` scrolling traits to section controllers via `IGListCollectionContext`. [Adam Stern](https://github.com/adamastern) [(b4c8ea1)](https://github.com/Instagram/IGListKit/commit/b4c8ea180f3bbda8b7995da758fbec58bf7214a8) - `IGListBindingSectionController` no longer asserts when reloading the entire section. A warning message is now logged if the entire section is going to be reloaded. [Jeff Bailey](https://github.com/jeffbailey) [(#1213)](https://github.com/Instagram/IGListKit/pull/1213) - Added `preferItemReloadsForSectionReloads` in IGListAdapterUpdater so that the item updates are invoked with the proper collectionView animation, instead of using the delete+insert section operation when the number of items is unchanged. [Zhisheng Huang](https://github.com/lorixx) [(f699ea0)](https://github.com/Instagram/IGListKit/commit/f699ea0e17a8cc6335285dc9f86fb80a0ad49700) - Created `IGListAdapterPerformanceDelegate` for IGListAdapter to be able to measure how long some operations take across all section controllers. For example, how long it takes to dequeue a cell. [Maxime Ollivier](https://github.com/maxoll) [(4662454)](https://github.com/Instagram/IGListKit/commit/4662454c4aedbc5d3bed0cb386f2ef93de40ba51) - Update CocoaPods integration to use the CocoaPods specs CDN [Koen Punt](https://github.com/koenpunt) [(#1386)](https://github.com/Instagram/IGListKit/pull/1386) - Remove useless system version code [Kinarobin](https://github.com/kinarobin) [(#1386)](https://github.com/Instagram/IGListKit/pull/1396) ### Fixes - Fixed bug with layouts inconsistency in `updateAnimated:completion` of IGListBindingSectionController. [Qinghua Hong](https://github.com/qhhonx) [(#1285)](https://github.com/Instagram/IGListKit/pull/1285) - Fixed bug with `-[IGListAdapter scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:animated:]` where the content inset(bottom/right) of the collection view was incorrectly being applied to the final offset and was inconsistent with the content inset(top/left) of the collection view being applied. [Qinghua Hong](https://github.com/qhhonx) [(#1284)](https://github.com/Instagram/IGListKit/pull/1284) - Fixed crash when the data source is nil before calling `-[IGListAdapterUpdater performUpdateWithCollectionViewBlock:fromObjects:toObjectsBlock:animated:objectTransitionBlock:completion:]`. [Zhisheng Huang](https://github.com/lorixx) [(6cdd112)](https://github.com/Instagram/IGListKit/commit/6cdd112790f13a683d3e061a7646f2c3549cc4dd) - Experimental fix to get the `UICollectionView` for batch updating immediately before applying the update. [Ryan Nystrom](https://github.com/rnystrom) [(583efb9)](https://github.com/Instagram/IGListKit/commit/583efb936b0ba4d8beac0678b145aa5aff8ac12b) - Fixed bug with `IGListDiff.mm` where arrays of `NSIndexPath`, instead of `NSIndexPath`, were incorrectly set as objects for the IndexPathMaps. [Bofei Zhu](https://github.com/zhubofei) [(#1205)](https://github.com/Instagram/IGListKit/pull/1205) - `[IGListAdapterUpdater performBatchUpdatesWithCollectionViewBlock:]` and `[IGListAdapterUpdater performReloadDataWithCollectionViewBlock:]` clean state and run completion blocks if their `UICollectionView` is nil. [Brandon Darin](https://github.com/jbd1030) [(290d592)](https://github.com/Instagram/IGListKit/commit/290d592983713c3ef82eb4950ba773a0059563a2) - Ensuring view models with duplicate diff identifiers are removed when view models are first requested by `IGListBindingSectionController` [Adam Stern](https://github.com/adamastern) [(a1ee4c1)](https://github.com/Instagram/IGListKit/commit/a1ee4c19f7a6cbd9899dba82deb5fb3ece669e9c) - Fixed `[IGListAdapterUpdater reloadItemInCollectionView:fromIndexPath:toIndexPath:]` does not call delegate when not inside a batch update. [Bofei Zhu](https://github.com/zhubofei) [(#1211)](https://github.com/Instagram/IGListKit/pull/1211) - Log instead of assert for duplicate diff identifiers to make code testable. [Adam Stern](https://github.com/adamastern) [(bee2178)](https://github.com/Instagram/IGListKit/commit/bee2178443ffcaff1d9135c4e094f74240433a62) - Removed `nibName` argument from `IGListReusableViewIdentifier`. [Trung Duc](https://github.com/trungducc) [(#1223)](https://github.com/Instagram/IGListKit/issues/1223) - Fixed crash when using `-[IGListCollectionContext dequeueReusableCellOfClass:withReuseIdentifier:forSectionController:atIndex:]` [Jeremy Lawrence](https://github.com/ziewvater) [(3b19cfb)](https://github.com/Instagram/IGListKit/commit/3b19cfb9d742d1fd97540bd8cf42c3552ab47de9) - Added missing method override to `IGListBindingSectionController` that updates the internal `viewModels` array after moving a cell. [Dennis Müller](https://github.com/d3mueller) [(#1262)](https://github.com/Instagram/IGListKit/issues/1262) - Fixed logic flaw in `[IGListCollectionViewLayout shouldInvalidateLayoutForBoundsChange:]`. [Allen Hsu](https://github.com/allenhsu) [(#1236)](https://github.com/Instagram/IGListKit/pull/1236) - Fixed crash when calling `[UICollectionView layoutAttributesForSupplementaryElementOfKind...]` with `IGListCollectionViewLayout` and the section controller doesn't actually return a supplementary view [Maxime Ollivier](https://github.com/maxolls) [(cddb297)](https://github.com/Instagram/IGListKit/commit/cddb29799c5393f3c1a1ab7e9c072208e8c23225) - Added `IGListExperimentAvoidLayoutOnScrollToObject` to avoid creating off-screen cells when calling `[IGListAdapter scrollToObject ...]`. [Maxime Ollivier](https://github.com/maxolls) [(6faddd9)](https://github.com/Instagram/IGListKit/commit/6faddd99c95428cf42bb38684464b458ef1455c0) - Added `IGListExperimentFixIndexPathImbalance` to test fixing a crash when inserting and deleting the same NSIndexPath multiple times. [Maxime Ollivier](https://github.com/maxolls) [(7824698)](https://github.com/Instagram/IGListKit/commit/78246986108e7caf73111fe784057cc107ee67f1) 3.4.0 ----- ### Enhancements - Relicensed IGListKit to MIT. [Ryan Nystrom](https://github.com/rnystrom) [(000bc36)](https://github.com/Instagram/IGListKit/commit/000bc3691909f50649a5dfb098a5f2102c86385b) - Experimental performance improvement from deferring `-[IGListAdapterDataSource objectsForListAdapter:]` calls until just before diffing. [Ryan Nystrom](https://github.com/rnystrom) [(3059c5e)](https://github.com/Instagram/IGListKit/commit/3059c5e6f5aeac73f112375d032677ae5f38342a) 3.3.0 ----- ### Enhancements - Add support for UICollectionView's interactive reordering in iOS 9+. Updates include `-[IGListSectionController canMoveItemAtIndex:]` to enable the behavior, `-[IGListSectionController moveObjectFromIndex:toIndex:]` called when items within a section controller were moved through reordering, `-[IGListAdapterDataSource listAdapter:moveObject:from:to]` called when section controllers themselves were reordered (only possible when all section controllers contain exactly 1 object), and `-[IGListUpdatingDelegate moveSectionInCollectionView:fromIndex:toIndex]` to enable custom updaters to conform to the reordering behavior. The update also includes two new examples `ReorderableSectionController` and `ReorderableStackedViewController` to demonstrate how to enable interactive reordering in your client app. [Jared Verdi](https://github.com/jverdi) [(#976)](https://github.com/Instagram/IGListKit/pull/976) - 5x improvement to diffing performance when result is only inserts or deletes. [Ryan Nystrom](https://github.com/rnystrom) [(afd2d29)](https://github.com/Instagram/IGListKit/commit/afd2d29eecfac2231d2bcf815c76e844c98d838e) - Can always show sticky header although section data is empty. [Marcus Wu](https://github.com/marcuswu0814) [(#1129)](https://github.com/Instagram/IGListKit/pull/1129) - Added `-[IGListCollectionContext dequeueReusableCellOfClass:withReuseIdentifier:forSectionController:atIndex:]` to allow for registering cells of the same class with different reuse identifiers. [Jeremy Lawrence](https://github.com/Ziewvater) [(f47753e)](https://github.com/Instagram/IGListKit/commit/f47753e3615431f3b079eb3b7900469f9ffdce5b) ### Fixes - Fixed Xcode 9.3 build errors. [Sho Ikeda](https://github.com/ikesyo) [(#1143)](https://github.com/Instagram/IGListKit/pull/1143) - Copy objects when retrieving from datasource to prevent modification of models in binding section controller. [Kashish Goel](https://github.com/kashishgoel) [(#1109)](https://github.com/Instagram/IGListKit/pull/1109) - Fixed footer is sticky when `stickyHeader` is `true` [aelam](https://github.com/aelam) [(#1094)](https://github.com/Instagram/IGListKit/pull/1094) - Updated IGListCollectionViewLayout to rely on layoutAttributesClass instead of vanilla `UICollectionViewLayoutAttributes` [Cole Potrocky](https://github.com/SirensOfTitan) [#1135](https://github.com/instagram/IGListKit/pull/1135) - `-[IGListSectionController didSelectItemAtIndex:]` is now called when a `scrollViewDelegate` or `collectionViewDelegate` is set. [Ryan Nystrom](https://github.com/rnystrom) [(#1108)](https://github.com/Instagram/IGListKit/pull/1108) - Fixed binding section controllers failing to update their cells when the section controller's section changes. [Chrisna Aing](https://github.com/ccrazy88) [(#1144)](https://github.com/Instagram/IGListKit/pull/1144) - Fixed a bug caused when applying interactive reordering on a single section item while dragging it through the last spot of the collection view and back to some (non-last) target position. [Ofir Gluzman](https://github.com/ofirgluzman) [#1289](https://github.com/Instagram/IGListKit/pull/1289) 3.2.0 ----- ### Enhancements - Added `-[IGListSectionController didHighlightItemAtIndex:]` and `-[IGListSectionController didUnhighlightItemAtIndex:]` APIs to support `UICollectionView` cell highlighting. [Kevin Delannoy](https://github.com/delannoyk) [(#933)](https://github.com/Instagram/IGListKit/pull/933) - Added `-didDeselectSectionController:withObject:` to `IGListSingleSectionControllerDelegate` [Darren Clark](https://github.com/darrenclark) [(#954)](https://github.com/Instagram/IGListKit/pull/954) - Added a new listener API to be notified when `IGListAdapter` finishes updating. Add listeners via `-[IGListAdapter addUpdateListener:]` with objects conforming to the new `IGListAdapterUpdateListener` protocol. [Ryan Nystrom](https://github.com/rnystrom) [(5cf01cc)](https://github.com/Instagram/IGListKit/commit/5cf01cc0a7c41d370600df495aff91d1099fa0bc) - Updated project settings for iOS 11. [Ryan Nystrom](https://github.com/rnystrom) [(#942)](https://github.com/Instagram/IGListKit/pull/942) - Added support UICollectionElementKindSectionFooter for IGListCollectionViewLayout. [Igor Vasilenko](https://github.com/vasilenkoigor) [(#1017)](https://github.com/Instagram/IGListKit/pull/1017) - Added experiment to make `-[IGListAdapter visibleSectionControllers:]` a bit faster. [Maxime Ollivier](https://github.com/maxoll) [(82a2a2e)](https://github.com/Instagram/IGListKit/commit/82a2a2ee18bb6272744fd14c64c8ff2da3a620a6) - Added support `-[UIScrollView adjustedContentInset]` for iOS 11. [Guoyin Li](https://github.com/yiplee) [(#1020)](https://github.com/Instagram/IGListKit/pull/1020) - Added new `transitionDelegate` API to give `IGListSectionController`s control to customize initial and final `UICollectionViewLayoutAttribute`s. Includes automatic integration with `IGListCollectionViewLayout`. Sue Suhan Ma [(26924ec)](https://github.com/Instagram/IGListKit/commit/26924ec3b665d37aeed7e28887e4221a7f3501b1) - Reordered position of intercepted selector in `IGListAdapterProxy`'s `isInterceptedSelector` method to reduce overall consumption of compare. [zhongwuzw](https://github.com/zhongwu) [(#1055)](https://github.com/Instagram/IGListKit/pull/1055) - Made IGListTransitionDelegate inherited from NSObject. [Igor Vasilenko](https://github.com/vasilenkoigor) [(#1075)](https://github.com/Instagram/IGListKit/pull/1075) ### Fixes - Duplicate objects for initial data source setup filtered out. [Mikhail Vashlyaev](https://github.com/yemodin) [(#993](https://github.com/Instagram/IGListKit/pull/993) - Weakly reference the `UICollectionView` in coalescence so that it can be released if the rest of system is destroyed. [Ryan Nystrom](https://github.com/rnystrom) [(d322c2e)](https://github.com/Instagram/IGListKit/commit/d322c2e5ae241141309923da257542f163c07cc6) - Fix bug with `-[IGListAdapter scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:animated:]` where the content inset of the collection view was incorrectly being applied to the final offset. [Ryan Nystrom](https://github.com/rnystrom) [(b2860c3)](https://github.com/Instagram/IGListKit/commit/b2860c3604f0c452be1d21ab09c771c921786150) - Avoid crash when invalidating the layout while inside `-[UICollectionView performBatchUpdates:completion:]. [Ryan Nystrom](https://github.com/rnystrom) [(d9a89c9)](https://github.com/Instagram/IGListKit/commit/d9a89c9b00aa1a9537a24d9affb6919f83065f65) - Duplicate view models in `IGListBindingSectionController` gets filtered out. [Weyert de Boer](https://github.com/weyert) [(#916)](https://github.com/Instagram/IGListKit/pull/916) - Check object type on lookup to prevent crossing types if different objects collide with their identifiers. [Ryan Nystrom](https://github.com/rnystrom) [(296baf5)](https://github.com/Instagram/IGListKit/commit/296baf5f854f57150ed12ca5bd8d3903db492734) 3.1.1 ----- ### Fixes - Prevent a crash when `IGListBindingSectionControllerDelegate` objects do not implement the optional deselection API. [Ryan Nystrom](https://github.com/rnystrom) [(#921)](https://github.com/Instagram/IGListKit/pull/921) 3.1.0 ----- ### Enhancements - Added debug descriptions for 'IGListBindingSectionController' when printing to lldb via `po [IGListDebugger dump]`. [Candance Smith](https://github.com/candance) [(#856)](https://github.com/Instagram/IGListKit/pull/856) - Added `-[IGListSectionController didDeselectItemAtIndex:]` API to support default `UICollectionView` cell deselection. [Ryan Nystrom](https://github.com/rnystrom) [(6540f96)](https://github.com/Instagram/IGListKit/commit/6540f960e2e69bd4776e1e1d8c460ff812ba4c07) - Added `-[IGListCollectionContext selectItemAtIndex:]` Select an item through IGListCollectionContext like `-[IGListCollectionContext deselectItemAtIndex:]`. [Marvin Nazari](https://github.com/MarvinNazari) [(#874)](https://github.com/Instagram/IGListKit/pull/874) - Added horizontal scrolling support to `IGListCollectionViewLayout`. [Peter Edmonston](https://github.com/edmonston) [(#857)](https://github.com/Instagram/IGListKit/pull/857) - Added support for `scrollViewDidEndDecelerating` to `IGListAdapter`. [Phil Larson](https://github.com/plarson) [(#899)](https://github.com/Instagram/IGListKit/pull/899) - Automatically disable `[UICollectionView isPrefetchingEnabled]` when setting a collection view on an adapter. [Ryan Nystrom](https://github.com/rnystrom) [(#889)](https://github.com/Instagram/IGListKit/pull/889) ### Fixes - Prevent a crash when update queued immediately after item batch update. [Ryan Nystrom](https://github.com/rnystrom) [(3dc6060)](https://github.com/Instagram/IGListKit/commit/3dc6060a385d9bfcb4fa1f61262ba74776573229) - Return correct `-[IGListAdapter visibleSectionControllers]` when section has no items, but has supplementary views. [Mani Ghasemlou](https://github.com/manicakes) [(#643)](https://github.com/Instagram/IGListKit/issues/643) - Call `[CATransaction commit]` before calling completion block in IGListAdapterUpdater to prevent animation issues. [Maxime Ollivier](https://github.com/maxoll) [(6f946b2)](https://github.com/Instagram/IGListKit/commit/6f946b2981d266f823324a366213bd214357bb6d) - Fix `scrollToObject:supplementaryKinds:...` not scrolling when section is empty but does have supplymentary views. [Gulam Moledina](https://github.com/gmoledina) [(#808)](https://github.com/Instagram/IGListKit/pull/808) - Better support for non-top positions in `scrollToObject:` API. [Gulam Moledina](https://github.com/gmoledina) [(#861)](https://github.com/Instagram/IGListKit/pull/861) 3.0.0 ----- This release closes the [3.0.0 milestone](https://github.com/Instagram/IGListKit/milestone/3). ### Breaking Changes - Added Swift annotation names which remove `IG` prefixes from class names, C functions, and other APIs. Note, this only affects Swift clients. [Robert Payne](https://github.com/robertjpayne) [(#593)](https://github.com/Instagram/IGListKit/pull/593) Example: ```swift // OLD class MySectionController : IGListSectionController { ... } // NEW class MySectionController : ListSectionController { ... } // OLD IGListDiff([], [], .equality) // NEW ListDiff(oldArray: [], newArray: [], .equality) ``` - Updated `didSelect` delegate call in `IGListSingleSectionControllerDelegate` to include object. [Sherlouk](https://github.com/Sherlouk) [(#397)](https://github.com/Instagram/IGListKit/pull/397) ```objc // OLD - (void)didSelectSingleSectionController:(IGListSingleSectionController *)sectionController; // NEW - (void)didSelectSectionController:(IGListSingleSectionController *)sectionController withObject:(id)object; ``` - `IGListUpdatingDelegate` now conforms to `NSObject`, bringing it in line with other framework protocols. [Adlai Holler](https://github.com/Adlai-Holler) [(#435)](https://github.com/Instagram/IGListKit/pull/435) - Changed `hasChanges` methods in `IGListIndexPathResult` and `IGListIndexSetResult` to read-only properties. [Bofei Zhu](https://github.com/zhubofei) [(#453)](https://github.com/Instagram/IGListKit/pull/453) - Replaced `IGListGridCollectionViewLayout` with `IGListCollectionViewLayout`. [Ryan Nystrom](https://github.com/rnystrom) ([#482](https://github.com/Instagram/IGListKit/pull/482), [#450](https://github.com/Instagram/IGListKit/pull/450)) - Renamed `IGListAdapterUpdaterDelegate` method to `listAdapterUpdater:didPerformBatchUpdates:collectionView:`. [Vincent Peng](https://github.com/vincent-peng) [(#491)](https://github.com/Instagram/IGListKit/pull/491) - Moved section controller mutations to `IGListBatchContext`, provided as a parameter when calling `-performBatchAnimated:updates:completion` on a section controller's `collectionContext`. All updates (insert, delete, reload item/section controller) must now be done inside a batch update block. [Ryan Nystrom](https://github.com/rnystrom) [(a15ea08)](https://github.com/Instagram/IGListKit/commit/a15ea0861492c8476bc9b1b92b0d9835814091c7) ```objc // OLD [self.collectionContext performBatchAnimated:YES updates:^{ self.expanded = YES; [self.collectionContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:1]]; } completion:nil]; // NEW [self.collectionContext performBatchAnimated:YES updates:^(id batchContext) { self.expanded = YES; [batchContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:1]]; } completion:nil]; // OLD [self.collectionContext reloadSectionController:self]; // NEW [self.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadSectionController:self]; } completion:nil]; ``` - `-[IGListCollectionContext containerSize]` no longer accounts for the content inset of the collection view when returning a size. If you require that behavior, you can now use `-[IGListCollectionContext insetContainerSize]`. [Ryan Nystrom](https://github.com/rnystrom) [(623ff2a)](https://github.com/Instagram/IGListKit/commit/623ff2a8a85e0e2e8d0331ae3250d67985cd06b6) - `IGListCollectionView` has been **completely removed** in favor of using plain old `UICollectionView`. See discussion at [#409](https://github.com/Instagram/IGListKit/issues/409) for details. [Jesse Squires](https://github.com/jessesquires) [(2284ce3)](https://github.com/Instagram/IGListKit/commit/2284ce389708f62d99f48ff2ec15644f1ec59537) - `IGListBatchUpdateData` replaced its `NSSet` properties with `NSArray` instead. [Ryan Nystrom](https://github.com/rnystrom) [(#616)](https://github.com/Instagram/IGListKit/pull/616) - `IGListUpdatingDelegate` now requires method `-reloadItemInCollectionView:fromIndexPath:toIndexPath:` to handle reloading cells between index paths. [Ryan Nystrom](https://github.com/rnystrom) [(#657)](https://github.com/Instagram/IGListKit/pull/657) - `-[IGListCollectionContext sectionForSectionController:]` has been removed and replaced with the `NSInteger sectionIndex` property on `IGListSectionController`. [Andrew Monshizadeh](https://github.com/amonshiz) [#671](http://github.com/Instagram/IGListKit/pull/671) ### Enhancements - Added an initializer on `IGListAdapter` that does not take a `workingRangeSize` and defaults it to 0. [BasThomas](https://github.com/BasThomas) [(#686)](https://github.com/Instagram/IGListKit/pull/686) - Added `-[IGListAdapter visibleCellsForObject:]` API. [Sherlouk](https://github.com/Sherlouk) [(#442)](https://github.com/Instagram/IGListKit/pull/442) - Added `-[IGListAdapter sectionControllerForSection:]` API. [Adlai-Holler](https://github.com/Adlai-Holler) [(#477)](https://github.com/Instagram/IGListKit/pull/477) - You can now manually move items (cells) within a section controller, ex: `[self.collectionContext moveInSectionController:self fromIndex:0 toIndex:1]`. [Ryan Nystrom](https://github.com/rnystrom) [(#418)](https://github.com/Instagram/IGListKit/pull/418) - Invalidate the layout of a section controller and control the transition with `UIView` animation APIs. [Ryan Nystrom](https://github.com/rnystrom) [(#499)](https://github.com/Instagram/IGListKit/pull/499) - Added `-[IGListAdapter visibleIndexPathsForSectionController:]` API. [Malecks](https://github.com/Malecks) [(#465)](https://github.com/Instagram/IGListKit/pull/465) - Added `IGListBindingSectionController` which automatically binds view models to cells and animates updates at the cell level. [Ryan Nystrom](https://github.com/rnystrom) [(#494)](https://github.com/Instagram/IGListKit/pull/494) - Added `IGListGenericSectionController` to take advantage of Objective-C (and Swift) generics and automatically store strongly-typed references to the object powering your section controller. [Ryan Nystrom](https://github.com/rnystrom) ([301f147](https://github.com/Instagram/IGListKit/commit/301f1471c9a7a802320e07890f5e98f15ada4e2e)) - Added a debug option for IGListKit that you can print to lldb via `po [IGListDebugger dump]`. [Ryan Nystrom](https://github.com/rnystrom) [(#617)](https://github.com/Instagram/IGListKit/pull/617) ### Fixes - Gracefully handle a `nil` section controller returned by an `IGListAdapterDataSource`. [Ryan Nystrom](https://github.com/rnystrom) [(#488)](https://github.com/Instagram/IGListKit/pull/488) - Fix bug where emptyView's hidden status is not updated after the number of items is changed with `insertInSectionController:atIndexes:` or related methods. [Peter Edmonston](https://github.com/edmonston) [(#395)](https://github.com/Instagram/IGListKit/pull/395) - Fix bug where `IGListStackedSectionController`'s children need to know `numberOrItems` before didUpdate is called. [(#348)](https://github.com/Instagram/IGListKit/pull/390) - Fix bug where `-[UICollectionViewCell ig_setStackedSectionControllerIndex:]` should use `OBJC_ASSOCIATION_COPY_NONATOMIC` for NSNumber. [PhilCai](https://github.com/PhilCai1993) [(#424)](https://github.com/Instagram/IGListKit/pull/426) - Fix potential bug with suppressing animations (by passing `NO`) during `-[IGListAdapter performUpdatesAnimated: completion:]` where user would see UI glitches/flashing. [Jesse Squires](https://github.com/jessesquires) [(019c990)](https://github.com/Instagram/IGListKit/commit/019c990312eea4203c7388a83b50685d426aa372) - Fix bug where scroll position would be incorrect in call to `-[IGListAdapter scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:animated:` with scrollDirection/scrollPosition of UICollectionViewScrollDirectionVertical/UICollectionViewScrollPositionCenteredVertically or UICollectionViewScrollDirectionHorizontal/UICollectionViewScrollPositionCenteredHorizontally and with a collection view with nonzero contentInset. [David Yamnitsky](https://github.com/nitsky) [(5cc0fcd)](https://github.com/Instagram/IGListKit/commit/5cc0fcd1d77d6296f57ce1c298301b9881cb4d4a) - Fix a crash when reusing collection views between embedded `IGListAdapter`s. [Ryan Nystrom](https://github.com/rnystrom) [(#517)](https://github.com/Instagram/IGListKit/pull/517) - Only collect batch updates when explicitly inside the batch update block, execute them otherwise. Fixes dropped updates. [Ryan Nystrom](https://github.com/rnystrom) [(#494)](https://github.com/Instagram/IGListKit/pull/494) - Remove objects that return `nil` diff identifiers before updating. [Ryan Nystrom](https://github.com/rnystrom) [(af984ca)](https://github.com/Instagram/IGListKit/commit/af984ca81d4d8c4ba3012be1a45f69670a832ccf) - Fix a potential crash when a section is moved and deleted at the same time. [Ryan Nystrom](https://github.com/rnystrom) [(#577)](https://github.com/Instagram/IGListKit/pull/577) - Prevent section controllers and supplementary sources from returning negative sizes that crash `UICollectionViewFlowLayout`. [Ryan Nystrom](https://github.com/rnystrom) [(#583)](https://github.com/Instagram/IGListKit/pull/583) - Add nullability annotations to a few more headers. [Adlai Holler](https://github.com/Adlai-Holler) [(#626)](https://github.com/Instagram/IGListKit/pull/626) - Fix a crash when inserting or deleting from the same index within the same batch-update application. [Ryan Nystrom](https://github.com/rnystrom) [(#616)](https://github.com/Instagram/IGListKit/pull/616) - `IGListSectionType` protocol was removed and its methods were absorted into the `IGListSectionController` base class with default implementations. [Ryan Nystrom](https://github.com/rnystrom) ([3102852](https://github.com/Instagram/IGListKit/commit/3102852ce258274e8727f9094695a9c331e1abf3)) - When setting the collection view on `IGListAdapter`, its layout is now properly invalidated. [Jesse Squires](https://github.com/jessesquires) [(#677)](https://github.com/Instagram/IGListKit/pull/677) - Fixes a bug when reusing `UICollectionView`s with multiple `IGListAdapter`s in an embedded environment that would accidentally `nil` the `collectionView` property of another adapter. [Ryan Nystrom](https://github.com/rnystrom) [(#721)](https://github.com/Instagram/IGListKit/pull/721) - Fixes a bug where maintaining a reference to a section controller but not the list adapter in an async block could lead to calling `-[IGListAdapter sectionForSectionController:]` (or checking `-[IGListSectionController sectionIndex]`) and receiving an incorrect value. With the adapter check the value would be 0 because the adapter was `nil` and for the section controller property the value would be the last set index value. [Andrew Monshizadeh](https://github.com/amonshiz) [(#709)](https://github.com/Instagram/IGListKit/issues/709) 2.1.0 ----- This release closes the [2.1.0 milestone](https://github.com/Instagram/IGListKit/milestone/2). ### Enhancements - Added support for macOS. Note: this is *only* for the Diffing components. There is **no support** for `IGListAdapter`, `IGListSectionController`, and other components at this time. [Guilherme Rambo](https://github.com/insidegui) [(#235)](https://github.com/Instagram/IGListKit/pull/235) - Added a [macOS example](https://github.com/Instagram/IGListKit/tree/main/Examples/Examples-macOS) project. [Guilherme Rambo](https://github.com/insidegui) [(#337)](https://github.com/Instagram/IGListKit/pull/337) - Disables `prefetchEnabled` by default on `IGListCollectionView`. [Sven Bacia](https://github.com/svenbacia) [(#323)](https://github.com/Instagram/IGListKit/pull/323) - Working ranges now work with `IGListStackedSectionController`. [Ryan Nystrom](https://github.com/rnystrom) [(#356)](https://github.com/Instagram/IGListKit/pull/356) - Added CocoaPods subspec for diffing, `IGListKit/Diffing` and an [installation guide](https://instagram.github.io/IGListKit/installation.html). [Sherlouk](https://github.com/Sherlouk) [(#368)](https://github.com/Instagram/IGListKit/pull/368) - Added `allowsBackgroundReloading` flag (default `YES`) to `IGListAdapterUpdater` so users can configure this behavior as needed. [Adlai-Holler](https://github.com/Adlai-Holler) [(#375)](https://github.com/Instagram/IGListKit/pull/375) - `-[IGListAdapter updater]` is now public (read-only). [Adlai-Holler](https://github.com/Adlai-Holler) [(#379)](https://github.com/Instagram/IGListKit/pull/379) ### Fixes - Avoid `UICollectionView` crashes when queueing a reload and insert/delete on the same item as well as reloading an item in a section that is animating. [Ryan Nystrom](https://github.com/rnystrom) [(#325)](https://github.com/Instagram/IGListKit/pull/325) - Prevent adapter data source from deallocating after queueing an update. [Ryan Nystrom](https://github.com/rnystrom) [(4cc91a2)](https://github.com/Instagram/IGListKit/commit/4cc91a25c8b262953e4f2d8e5dc78ee15c6265b2) - Fix out-of-bounds bug when child section controllers in a stack remove cells. [Ryan Nystrom](https://github.com/rnystrom) [(#358)](https://github.com/Instagram/IGListKit/pull/358) - Fix a grid layout bug when item has full-width and iter-item spacing is not zero. [Bofei Zhu](https://github.com/zhubofei) [(#361)](https://github.com/Instagram/IGListKit/pull/361) 2.0.0 ----- This release closes the [2.0.0 milestone](https://github.com/Instagram/IGListKit/milestone/1?closed=1). We've increased test coverage to 97%. Thanks to the [27 contributors](https://github.com/Instagram/IGListKit/graphs/contributors) who helped with this release! You can find a [migration guide here](https://instagram.github.io/IGListKit/migration.html) to assist with migrating between 1.0 and 2.0. ### Breaking Changes - Diff result method on `IGListIndexPathResult` changed. `-resultWithUpdatedMovesAsDeleteInserts` was removed and replaced with `-resultForBatchUpdates` [(b5aa5e3)](https://github.com/Instagram/IGListKit/commit/b5aa5e39002854c947e777c11ae241f67f24d19c) ``` // OLD - (IGListIndexPathResult *)resultWithUpdatedMovesAsDeleteInserts; // NEW - (IGListIndexPathResult *)resultForBatchUpdates; ``` - `IGListDiffable` equality method changed from `isEqual:` to `isEqualToDiffableObject:` [(ab890fc)](https://github.com/Instagram/IGListKit/commit/ab890fc6070f170a2db5a383a6296e62dcf75678) - The default `NSObject` category was removed and replaced with `NSString` and `NSNumber` categories. All other models will need to conform to `IGListDiffable`. [(3947600)](https://github.com/Instagram/IGListKit/commit/394760081c7c2daa5ae6c18e00cdeaf2b67e22c1) - Added support for specifying an end position when scrolling. [Bofei Zhu](https://github.com/zhubofei) [(#196)](https://github.com/Instagram/IGListKit/pull/196). The `IGListAdapter` scrolling method changed: ```objc // OLD - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection animated:(BOOL)animated; // NEW - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection scrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; ``` ### Fixes - Consider supplementary views with display and end-display events. [Ryan Nystrom](https://github.com/rnystrom) [(#470)](https://github.com/Instagram/IGListKit/pull/470) - Changed `NSUInteger` to `NSInteger` in all public APIs. [Suraya Shivji](https://github.com/surayashivji) [(#200)](https://github.com/Instagram/IGListKit/issues/200) ### Enhancements - Added support for supplementaryViews created from nibs. [Rawlinxx](https://github.com/rawlinxx) [(#90)](https://github.com/Instagram/IGListKit/pull/90) - Added support for cells created from nibs. [Sven Bacia](https://github.com/svenbacia) [(#56)](https://github.com/Instagram/IGListKit/pull/56) - Added an additional initializer for `IGListSingleSectionController` to be able to support single sections created from nibs. An example can be found [here](https://github.com/Instagram/IGListKit/tree/main/Examples/Examples-iOS/IGListKitExamples/ViewControllers/SingleSectionViewController.swift). [(#56)](https://github.com/Instagram/IGListKit/pull/56) ```objc - (instancetype)initWithNibName:(NSString *)nibName bundle:(nullable NSBundle *)bundle configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; ``` - Added `-isFirstSection` and `-isLastSection` APIs to `IGListSectionController` [(316fbe2)](https://github.com/Instagram/IGListKit/commit/316fbe2b8b2508b58a0f38387c3a343b9c37e282) - Added support for cells and supplementaryViews created from storyboard. There's a new required method on the `IGListCollectionContext` protocol to do this. [Bofei Zhu](https://github.com/zhubofei) [(#92)](https://github.com/Instagram/IGListKit/pull/92) ```objc // IGListCollectionContext - (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; ``` - Added `tvOS` support. [Jesse Squires](https://github.com/jessesquires) [(#137)](https://github.com/Instagram/IGListKit/pull/137) - Added `-[IGListAdapter visibleObjects]` API. [Ryan Nystrom](https://github.com/rnystrom) [(386ae07)](https://github.com/Instagram/IGListKit/commit/386ae0786445c06e1eabf074a4181614332f155f) - Added `-[IGListAdapter objectForSectionController:]` API. [Ayush Saraswat](https://github.com/saraswatayu) [(#204)](https://github.com/Instagram/IGListKit/pull/204) - Added `IGListGridCollectionViewLayout`, a section-based grid layout. [Bofei Zhu](https://github.com/zhubofei) [(#225)](https://github.com/Instagram/IGListKit/pull/225) - Added support for scrolling to an index in a section controller from within that section controller. There's a new required method on the `IGListCollectionContext` protocol to do this. [Jesse Squires](https://github.com/jessesquires) [(e5afb5b)](https://github.com/Instagram/IGListKit/commit/e5afb5b4d0cfc70a2736b02279b6bc239ddf1e5d) ```objc // IGListCollectionContext - (void)scrollToSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index scrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; ``` ### Fixes - Fixed `-[IGListAdapter reloadDataWithCompletion:]` not returning early when `collectionView` or `dataSource` is `nil` and `completion` is `nil`. [Ben Asher](https://github.com/benasher44) [(#51)](https://github.com/Instagram/IGListKit/pull/51) - Prevent `UICollectionView` bug when accessing a cell during working range updates. [Ryan Nystrom](https://github.com/rnystrom) [(#216)](https://github.com/Instagram/IGListKit/pull/216) - Skip reloading for objects that are not found when calling `-[IGListAdapter reloadObjects:]`. [Ryan Nystrom](https://github.com/rnystrom) [(ca15e29)](https://github.com/Instagram/IGListKit/commit/ca15e29cf1dadc6c396fe8f14f16c27f6a38519c) - Fixes a crash when a reload is queued for an object that is deleted in the same runloop turn. [Ryan Nystrom](https://github.com/rnystrom) [(7c3d499)](https://github.com/Instagram/IGListKit/commit/7c3d4999ebde36ee4666e5aee99716d1ed1fb2d8) - Fixed a bug where `IGListStackSectionController` would only set its supplementary source once. [Ryan Nystrom](https://github.com/rnystrom) [(#286)](https://github.com/Instagram/IGListKit/pull/286) - Fixed a bug where `IGListStackSectionController` passed the wrong section controller for will-drag scroll events. [Ryan Nystrom](https://github.com/rnystrom) [(#286)](https://github.com/Instagram/IGListKit/pull/286) - Fixed a crash when deselecting a cell through a child section controller in an `IGListStackSectionController`. [Ryan Nystrom](https://github.com/rnystrom) [(#295)](https://github.com/Instagram/IGListKit/pull/295) ### Documentation - We now have 100% documentation coverage. Docs been refined and clarified. [Jesse Squires](https://github.com/jessesquires) [(#207)](https://github.com/Instagram/IGListKit/pull/207) - Added new Guides: [Getting Started](https://instagram.github.io/IGListKit/getting-started.html), [Migration](https://instagram.github.io/IGListKit/migration.html) - Added examples for Today & iMessage extensions. [Sherlouk](https://github.com/Sherlouk) [(#112)](https://github.com/Instagram/IGListKit/pull/112) - Added `tvOS` example pack. [Sherlouk](https://github.com/Sherlouk) [(#141)](https://github.com/Instagram/IGListKit/pull/141) 1.0.0 ----- Initial release. :tada: ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: Dangerfile ================================================ not_declared_trivial = !(github.pr_title.include? "#trivial") has_source_changes = !git.modified_files.grep(/Source/).empty? # Make it more obvious that a PR is a work in progress and shouldn't be merged yet warn("PR is classed as Work in Progress") if github.pr_title.include? "[WIP]" # Warn when there is a big PR warn("Big PR") if git.lines_of_code > 500 # Changelog entries are required for changes to library files no_changelog_entry = !git.modified_files.include?("CHANGELOG.md") if has_source_changes && no_changelog_entry && not_declared_trivial && git.lines_of_code > 10 fail("Any source code changes should have an entry in CHANGELOG.md.") end # Milestones are required to track what's included in each release if has_source_changes && not_declared_trivial has_milestone = !github.pr_json['milestone'].nil? warn('All pull requests should have a milestone attached, unless marked *#trivial*.', sticky: false) unless has_milestone end # Docs are regenerated when releasing has_doc_changes = !git.modified_files.grep(/docs\//).empty? has_doc_gen_title = github.pr_title.include? "#docgen" if has_doc_changes && !has_doc_gen_title fail("Docs are regenerated when creating new releases.") message("Docs are generated by using [Jazzy](https://github.com/realm/jazzy). If you want to contribute, please update [markdown guides](https://github.com/Instagram/IGListKit/tree/main/Guides)") end # Warn if Source files were added or removed but examples are not updated added_source_files = !git.added_files.grep(/Source/).empty? deleted_source_files = !git.deleted_files.grep(/Source/).empty? ios_pods_not_updated = !git.modified_files.include?("Examples/Examples-iOS/Podfile.lock") macos_pods_not_updated = !git.modified_files.include?("Examples/Examples-macOS/Podfile.lock") tvos_pods_not_updated = !git.modified_files.include?("Examples/Examples-tvOS/Podfile.lock") if (added_source_files || deleted_source_files) && (ios_pods_not_updated || macos_pods_not_updated || tvos_pods_not_updated) warn("Adding or removing library source files requires updating the examples. Please run `./scripts/pod_setup.sh` from the root directory and commit the changes.") end swiftlint.verbose = true swiftlint.config_file = '.swiftlint.yml' swiftlint.lint_files(inline_mode: true) ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/AppDelegate.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var isLaunched = false var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let demosViewController = DemosViewController() let splitViewController = UISplitViewController() splitViewController.delegate = self splitViewController.viewControllers = [UINavigationController(rootViewController: demosViewController)] splitViewController.preferredDisplayMode = .oneBesideSecondary window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = splitViewController window?.makeKeyAndVisible() UICollectionView.appearance().backgroundColor = UIColor.background return true } } extension AppDelegate: UISplitViewControllerDelegate { func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool { // We set up 2 view controllers on launch to enable the split view controller when launching on iPad. // However, for iPhone, discard the second view controller so the Demos view controller is visible at launch. if !isLaunched { isLaunched = true return true } return false } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/DelegateProtocols/PostSectionControllerDelegate.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit /// To allow communication between PostSectionController and FeedViewController protocol PostSectionControllerDelegate: AnyObject { func postSectionController(_ sectionController: PostSectionController, didSelectOptionsFor post: Post, from sourceView: UIView) } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Extensions/UIActivityIndicatorView+Extension.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit extension UIActivityIndicatorView { class var defaultStyle: UIActivityIndicatorView.Style { if #available(iOS 13.0, *) { return .medium } else { return .gray } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Extensions/UIColor+Extension.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit extension UIColor { class var background: UIColor { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } } class var groupedBackground: UIColor { if #available(iOS 13.0, *) { return .systemGroupedBackground } else { return .groupTableViewBackground } } class var secondaryGroupedBackground: UIColor { if #available(iOS 13.0, *) { return .secondarySystemGroupedBackground } else { return .white } } class var secondaryBackground: UIColor { if #available(iOS 13.0, *) { return .secondarySystemBackground } else { return .lightGray } } class var defaultSeparator: UIColor { if #available(iOS 13.0, *) { return UIColor.separator } else { return UIColor(red: 200 / 255.0, green: 199 / 255.0, blue: 204 / 255.0, alpha: 1) } } class var titleLabel: UIColor { if #available(iOS 13.0, *) { return .label } else { return .darkText } } class var detailLabel: UIColor { if #available(iOS 13.0, *) { return .secondaryLabel } else { return .lightGray } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/IGListKitExamples.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName IGListKit CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/APIService.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation // MARK: - Data Provider // In IGListKit architecture, the data provider is separated from the UI components // This enables clean separation of concerns and makes testing easier class APIService { static let shared = APIService() // Pagination state private var currentPage = 1 private var isLoading = false private var hasMoreData = true // Fetch posts with pagination support // This is called by the view controller to load data // IGListKit will handle the diffing and UI updates based on the results func fetchPosts(completion: @escaping ([Post]) -> Void) { guard !isLoading, hasMoreData else { return } self.isLoading = true // Simulate network delay DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { let posts = self.generateMockPosts(page: self.currentPage) if self.currentPage >= 5 { self.hasMoreData = false } self.currentPage += 1 self.isLoading = false DispatchQueue.main.async { completion(posts) } } } // Reset pagination state for refreshing func resetPagination() { self.currentPage = 1 self.hasMoreData = true } // Generate mock data for the demo app // In a real app, this would be replaced with API calls func generateMockPosts(page: Int) -> [Post] { let baseCount = (page - 1) * 5 return (1...5).map { index in let id = "\(baseCount + index)" return Post( id: id, username: "user\(Int.random(in: 100...999))", userAvatarURL: URL(string: "https://randomuser.me/api/portraits/men/\(Int.random(in: 1...99)).jpg"), imageURL: URL(string: "https://picsum.photos/id/\(baseCount + index + 10)/500/500"), title: "Post #\(id)", description: "This is a beautiful photo I took while traveling. What do you think? #travel #photography #nature", likes: Int.random(in: 10...1000), timeStamp: Date().addingTimeInterval(-Double(Int.random(in: 1...86400) * (baseCount + index))) ) } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/ActivityItem.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit final class ActivityItem: ListDiffable { let bodyText: String let header: String? let footer: String? init(bodyText: String, header: String? = nil, footer: String? = nil) { self.bodyText = bodyText self.header = header self.footer = footer } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return bodyText as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? ActivityItem else { return false } return bodyText == object.bodyText } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/FeedItem.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit final class FeedItem: ListDiffable { let pk: Int let user: User let comments: [String] init(pk: Int, user: User, comments: [String]) { self.pk = pk self.user = user self.comments = comments } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return pk as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? FeedItem else { return false } return user.isEqual(toDiffableObject: object.user) && comments == object.comments } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/GridItem.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import Foundation final class GridItem: NSObject { let color: UIColor let itemCount: Int var items: [String] = [] init(color: UIColor, itemCount: Int) { self.color = color self.itemCount = itemCount super.init() self.items = computeItems() } private func computeItems() -> [String] { return [Int](1...itemCount).map { String(describing: $0) } } } extension GridItem: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return self === object ? true : self.isEqual(object) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/HorizontalCardsSection.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import Foundation final class HorizontalCardsSection: NSObject { let cardCount: Int private(set) var items: [String] = [] init(cardCount: Int) { self.cardCount = cardCount super.init() self.items = computeItems() } private func computeItems() -> [String] { return [Int](1...cardCount).map { String(describing: $0) } } } extension HorizontalCardsSection: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return self === object ? true : self.isEqual(object) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/LoadingCellModel.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit import IGListKit final class LoadingCellModel { let identifier = "loading-cell" } // MARK: - ListDiffable Implementation // Even simple models like this loading indicator need to conform to ListDiffable // in order to be used with IGListKit extension LoadingCellModel: ListDiffable { // The diffIdentifier uniquely identifies this object // For a singleton loading indicator, a static string ID is sufficient func diffIdentifier() -> any NSObjectProtocol { return self.identifier as NSObjectProtocol } // isEqual compares properties that affect the visual representation // For this simple case, comparing identifiers is enough func isEqual(toDiffableObject object: (any ListDiffable)?) -> Bool { guard let object = object as? LoadingCellModel else { return false } return self.identifier == object.identifier } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/Month.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation import IGListKit final class Month { let name: String let days: Int // day int mapped to an array of appointment names let appointments: [Int: [NSString]] init(name: String, days: Int, appointments: [Int: [NSString]]) { self.name = name self.days = days self.appointments = appointments } } extension Month: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return name as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return true } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/Post.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @import IGListDiffKit; @interface Post : NSObject @property (nonatomic, strong, readonly) NSString *username; @property (nonatomic, strong, readonly) NSArray *comments; - (instancetype)initWithUsername:(NSString *)username comments:(NSArray *)comments NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/Post.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "Post.h" @implementation Post - (instancetype)initWithUsername:(NSString *)username comments:(NSArray *)comments { if (self = [super init]) { _username = [username copy]; _comments = [comments copy]; } return self; } #pragma mark - IGListDiffable - (id)diffIdentifier { return self; } - (BOOL)isEqualToDiffableObject:(id)object { // since the diff identifier returns self, object should only be compared with same instance return self == object; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/PostModel.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class Post { let id: String let username: String let userAvatarURL: URL? let imageURL: URL? let title: String let description: String let likes: Int let timeStamp: Date init(id: String, username: String, userAvatarURL: URL?, imageURL: URL?, title: String, description: String, likes: Int, timeStamp: Date) { self.id = id self.username = username self.userAvatarURL = userAvatarURL self.imageURL = imageURL self.title = title self.description = description self.likes = likes self.timeStamp = timeStamp } } // MARK: - ListDiffable Implementation // ListDiffable is the core protocol in IGListKit for data diffing // It's similar to Equatable but with more specific requirements for efficient diffing extension Post: ListDiffable { // This method returns a unique identifier for the object // IGListKit uses this to track objects across updates // It should be unique and stable across updates (like a database ID) func diffIdentifier() -> NSObjectProtocol { return self.id as NSObjectProtocol } // This method compares all properties that might cause visual changes // If this returns false for objects with the same diffIdentifier, // IGListKit will reload that section instead of leaving it alone func isEqual(toDiffableObject object: (any ListDiffable)?) -> Bool { guard let object = object as? Post else { return false } return self.id == object.id && self.username == object.username && self.userAvatarURL == object.userAvatarURL && self.imageURL == object.imageURL && self.title == object.title && self.description == object.description && self.likes == object.likes } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/RemodelGeneratedModels/PersonModel.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * This file is generated using the remodel generation script. * The name of the input file is PersonModel.value */ #import @import IGListDiffKit; @interface PersonModel : NSObject @property (nonatomic, readonly, copy) NSString *firstName; @property (nonatomic, readonly, copy) NSString *lastName; @property (nonatomic, readonly, copy) NSString *uniqueId; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId NS_DESIGNATED_INITIALIZER; @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/RemodelGeneratedModels/PersonModel.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** * This file is generated using the remodel generation script. * The name of the input file is PersonModel.value */ #if ! __has_feature(objc_arc) #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). #endif #import "PersonModel.h" @implementation PersonModel - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId { if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _uniqueId = [uniqueId copy]; } return self; } - (id)copyWithZone:(nullable NSZone *)zone { return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ - \n\t firstName: %@; \n\t lastName: %@; \n\t uniqueId: %@; \n", [super description], _firstName, _lastName, _uniqueId]; } - (id)diffIdentifier { return _uniqueId; } - (NSUInteger)hash { NSUInteger subhashes[] = {[_firstName hash], [_lastName hash], [_uniqueId hash]}; NSUInteger result = subhashes[0]; for (int ii = 1; ii < 3; ++ii) { unsigned long long base = (((unsigned long long)result) << 32 | subhashes[ii]); base = (~base) + (base << 18); base ^= (base >> 31); base *= 21; base ^= (base >> 11); base += (base << 6); base ^= (base >> 22); result = base; } return result; } - (BOOL)isEqual:(PersonModel *)object { if (self == object) { return YES; } else if (self == nil || object == nil || ![object isKindOfClass:[self class]]) { return NO; } return (_firstName == object->_firstName ? YES : [_firstName isEqual:object->_firstName]) && (_lastName == object->_lastName ? YES : [_lastName isEqual:object->_lastName]) && (_uniqueId == object->_uniqueId ? YES : [_uniqueId isEqual:object->_uniqueId]); } - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/RemodelGeneratedModels/PersonModel.value ================================================ PersonModel includes(IGListDiffable) { NSString *firstName NSString *lastName %diffIdentifier NSString *uniqueId } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/SelectionModel.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit enum SelectionModelType: Int { case none, fullWidth, nib } final class SelectionModel: NSObject { let options: [String] let type: SelectionModelType init(options: [String], type: SelectionModelType = .none) { self.options = options self.type = type } } extension SelectionModel: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return isEqual(object) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/SwipeActionSection.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import Foundation final class SwipeActionSection: NSObject { } extension SwipeActionSection: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return self === object ? true : self.isEqual(object) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/User.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit final class User: ListDiffable { let pk: Int let name: String let handle: String init(pk: Int, name: String, handle: String) { self.pk = pk self.name = name self.handle = handle } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return pk as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? User else { return false } return name == object.name && handle == object.handle } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/ViewModels/DayViewModel.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation import IGListKit final class DayViewModel { let day: Int let today: Bool let selected: Bool let appointments: Int init(day: Int, today: Bool, selected: Bool, appointments: Int) { self.day = day self.today = today self.selected = selected self.appointments = appointments } } extension DayViewModel: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return day as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { if self === object { return true } guard let object = object as? DayViewModel else { return false } return today == object.today && selected == object.selected && appointments == object.appointments } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Models/ViewModels/MonthTitleViewModel.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation import IGListKit final class MonthTitleViewModel { let name: String init(name: String) { self.name = name } } extension MonthTitleViewModel: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return name as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { if self === object { return true } guard object is MonthTitleViewModel else { return false } // name is checked in the diffidentifier, so we can assume its equal return true } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/DemoSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class DemoItem: NSObject { let name: String let imageName: String let controllerClass: UIViewController.Type let controllerIdentifier: String? init( name: String, imageName: String, controllerClass: UIViewController.Type, controllerIdentifier: String? = nil ) { self.name = name self.imageName = imageName self.controllerClass = controllerClass self.controllerIdentifier = controllerIdentifier } } extension DemoItem: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return name as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { if self === object { return true } guard let object = object as? DemoItem else { return false } return controllerClass == object.controllerClass && controllerIdentifier == object.controllerIdentifier } } final class DemoSectionController: ListSectionController { private var object: DemoItem? override func sizeForItem(at index: Int) -> CGSize { guard let context = collectionContext else { return .zero } let inset = context.containerInset let safeArea = viewController?.view.safeAreaInsets ?? .zero let width = context.containerSize.width - (inset.left + inset.right + safeArea.left + safeArea.right) return CGSize(width: width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object?.name cell.imageName = object?.imageName cell.style = .grouped cell.isTopCell = isFirstSection cell.isBottomCell = isLastSection if let splitViewController = viewController?.splitViewController { cell.disclosureImageView.isHidden = splitViewController.viewControllers.count > 1 } cell.separator.isHidden = cell.isSelected return cell } override func didUpdate(to object: Any) { self.object = object as? DemoItem } override func didSelectItem(at index: Int) { setSeparatorsHidden(true) let navigationController = UINavigationController() navigationController.navigationBar.prefersLargeTitles = true if let identifier = object?.controllerIdentifier { let storyboard = UIStoryboard(name: "Demo", bundle: nil) let controller = storyboard.instantiateViewController(withIdentifier: identifier) controller.title = object?.name navigationController.viewControllers = [controller] viewController?.showDetailViewController(navigationController, sender: self) } else if let controller = object?.controllerClass.init() { controller.title = object?.name navigationController.viewControllers = [controller] viewController?.showDetailViewController(navigationController, sender: self) } } override func didDeselectItem(at index: Int) { setSeparatorsHidden(false) } private func setSeparatorsHidden(_ hidden: Bool) { if let cell = collectionContext.cellForItem(at: 0, sectionController: self) as? LabelCell { cell.separator.isHidden = hidden } if section > 0, let listAdapter = collectionContext as? ListAdapter, let previousSectionController = listAdapter.sectionController(forSection: section - 1), let previousCell = collectionContext.cellForItem(at: 0, sectionController: previousSectionController) as? LabelCell { previousCell.separator.isHidden = hidden } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/DisplaySectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class DisplaySectionController: ListSectionController, ListDisplayDelegate { override init() { super.init() displayDelegate = self inset = UIEdgeInsets(top: 0, left: 0, bottom: 30, right: 0) } override func numberOfItems() -> Int { return 4 } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = "Section \(self.section), cell \(index)" return cell } // MARK: ListDisplayDelegate func listAdapter(_ listAdapter: ListAdapter, willDisplay sectionController: ListSectionController) { print("Will display section \(self.section)") } func listAdapter(_ listAdapter: ListAdapter, willDisplay sectionController: ListSectionController, cell: UICollectionViewCell, at index: Int) { print("Did will display cell \(index) in section \(self.section)") } func listAdapter(_ listAdapter: ListAdapter, didEndDisplaying sectionController: ListSectionController) { print("Did end displaying section \(self.section)") } func listAdapter(_ listAdapter: ListAdapter, didEndDisplaying sectionController: ListSectionController, cell: UICollectionViewCell, at index: Int) { print("Did end displaying cell \(index) in section \(self.section)") } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/EmbeddedSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class EmbeddedSectionController: ListSectionController { private var number: Int? override init() { super.init() self.inset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) } override func sizeForItem(at index: Int) -> CGSize { let height = collectionContext?.containerSize.height ?? 0 return CGSize(width: height, height: height) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CenterLabelCell = collectionContext.dequeueReusableCell(for: self, at: index) let value = number ?? 0 cell.text = "\(value + 1)" cell.backgroundColor = UIColor(red: 237 / 255.0, green: 73 / 255.0, blue: 86 / 255.0, alpha: 1) return cell } override func didUpdate(to object: Any) { number = object as? Int } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/ExpandableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class ExpandableSectionController: ListSectionController { private var expanded = false private var object: String? override func sizeForItem(at index: Int) -> CGSize { let width = collectionContext!.containerSize.width let height = expanded ? LabelCell.textHeight(object ?? "", width: width) : LabelCell.singleLineHeight return CGSize(width: width, height: height) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object return cell } override func didUpdate(to object: Any) { self.object = object as? String } override func didSelectItem(at index: Int) { expanded = !expanded UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: { self.collectionContext?.invalidateLayout(for: self) }) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/FeedItemSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit final class FeedItemSectionController: ListSectionController, ListSupplementaryViewSource { private var feedItem: FeedItem! override init() { super.init() supplementaryViewSource = self } // MARK: IGListSectionController Overrides override func numberOfItems() -> Int { return feedItem.comments.count } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = feedItem.comments[index] return cell } override func didUpdate(to object: Any) { feedItem = object as? FeedItem } // MARK: ListSupplementaryViewSource func supportedElementKinds() -> [String] { return [UICollectionView.elementKindSectionHeader, UICollectionView.elementKindSectionFooter] } func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView { switch elementKind { case UICollectionView.elementKindSectionHeader: return userHeaderView(atIndex: index) case UICollectionView.elementKindSectionFooter: return userFooterView(atIndex: index) default: fatalError() } } func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 40) } // MARK: Private private func userHeaderView(atIndex index: Int) -> UICollectionReusableView { let view: UserHeaderView = collectionContext.dequeueReusableSupplementaryView( ofKind: UICollectionView.elementKindSectionHeader, forSectionController: self, nibName: "UserHeaderView", bundle: nil, atIndex: index) view.handle = "@" + feedItem.user.handle view.name = feedItem.user.name return view } private func userFooterView(atIndex index: Int) -> UICollectionReusableView { let view: UserFooterView = collectionContext.dequeueReusableSupplementaryView( ofKind: UICollectionView.elementKindSectionFooter, forSectionController: self, nibName: "UserFooterView", bundle: nil, atIndex: index) view.commentsCount = "\(feedItem.comments.count)" return view } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/GridSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class GridSectionController: ListSectionController { private var object: GridItem? private let isReorderable: Bool required init(isReorderable: Bool = false) { self.isReorderable = isReorderable super.init() self.minimumInteritemSpacing = 1 self.minimumLineSpacing = 1 } override func numberOfItems() -> Int { return object?.itemCount ?? 0 } override func sizeForItem(at index: Int) -> CGSize { let itemsPerRow = 4.0 let width = (collectionContext?.containerSize.width ?? 0) - ((itemsPerRow - 1) * self.minimumLineSpacing) let itemSize = floor(width / itemsPerRow) return CGSize(width: itemSize, height: itemSize) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CenterLabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object?.items[index] ?? "undefined" cell.backgroundColor = object?.color return cell } override func didUpdate(to object: Any) { self.object = object as? GridItem } override func canMoveItem(at index: Int) -> Bool { return isReorderable } override func moveObject(from sourceIndex: Int, to destinationIndex: Int) { guard let object = object else { return } let item = object.items.remove(at: sourceIndex) object.items.insert(item, at: destinationIndex) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/HorizontalSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class HorizontalSectionController: ListSectionController, ListAdapterDataSource { private var number: Int? lazy var adapter: ListAdapter = { let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self.viewController) adapter.dataSource = self return adapter }() override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 100) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: EmbeddedCollectionViewCell = collectionContext.dequeueReusableCell(for: self, at: index) adapter.collectionView = cell.collectionView return cell } override func didUpdate(to object: Any) { number = object as? Int } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { guard let number = number else { return [] } return (0.. ListSectionController { return EmbeddedSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/LabelSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class LabelSectionController: ListSectionController { private var object: String? override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object return cell } override func didUpdate(to object: Any) { self.object = String(describing: object) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/ListeningSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit final class ListeningSectionController: ListSectionController, IncrementListener { private var value: Int = 0 init(announcer: IncrementAnnouncer) { super.init() announcer.addListener(listener: self) } func configureCell(cell: LabelCell) { cell.text = "Section: \(self.section), value: \(value)" } // MARK: ListSectionController Overrides override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) configureCell(cell: cell) return cell } // MARK: IncrementListener func didIncrement(announcer: IncrementAnnouncer, value: Int) { self.value = value guard let cell = collectionContext?.cellForItem(at: 0, sectionController: self) as? LabelCell else { return } configureCell(cell: cell) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/LoadingSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit // MARK: - LoadingSectionController // A simpler section controller that manages the loading indicator // Note that each type of content gets its own section controller // This is a key concept in IGListKit - each model type gets a dedicated controller final class LoadingSectionController: ListSectionController { override init() { super.init() self.inset = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 0) } // Set the size for the loading indicator cell override func sizeForItem(at index: Int) -> CGSize { let width = collectionContext?.containerSize.width ?? 0 return CGSize(width: width, height: 60) } // Create and return the loading cell override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell = collectionContext?.dequeueReusableCell(of: LoadingCell.self, for: self, at: index) else { fatalError("Failed to dequeue LoadingCell") } return cell } // Nothing to update for this simple controller override func didUpdate(to object: Any) { } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/MonthSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class MonthSectionController: ListBindingSectionController, ListBindingSectionControllerDataSource, ListBindingSectionControllerSelectionDelegate { private var selectedDay: Int = -1 override init() { super.init() dataSource = self selectionDelegate = self } // MARK: ListBindingSectionControllerDataSource func sectionController(_ sectionController: ListBindingSectionController, viewModelsFor object: Any) -> [ListDiffable] { guard let month = object as? Month else { return [] } let date = Date() let today = Calendar.current.component(.day, from: date) var viewModels = [ListDiffable]() viewModels.append(MonthTitleViewModel(name: month.name)) for day in 1..<(month.days + 1) { let viewModel = DayViewModel( day: day, today: day == today, selected: day == selectedDay, appointments: month.appointments[day]?.count ?? 0 ) viewModels.append(viewModel) } for appointment in month.appointments[selectedDay] ?? [] { viewModels.append(appointment) } return viewModels } func sectionController(_ sectionController: ListBindingSectionController, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable { switch viewModel { case is DayViewModel: return collectionContext.dequeueReusableCell(for: self, at: index) as CalendarDayCell case is MonthTitleViewModel: return collectionContext.dequeueReusableCell(for: self, at: index) as MonthTitleCell default: return collectionContext.dequeueReusableCell(for: self, at: index) as LabelCell } } func sectionController(_ sectionController: ListBindingSectionController, sizeForViewModel viewModel: Any, at index: Int) -> CGSize { guard let width = collectionContext?.containerSize.width else { return .zero } if viewModel is DayViewModel { let square = width / 7.0 return CGSize(width: square, height: square) } else if viewModel is MonthTitleViewModel { return CGSize(width: width, height: 30.0) } else { return CGSize(width: width, height: 55.0) } } // MARK: ListBindingSectionControllerSelectionDelegate func sectionController(_ sectionController: ListBindingSectionController, didSelectItemAt index: Int, viewModel: Any) { guard let dayViewModel = viewModel as? DayViewModel else { return } if dayViewModel.day == selectedDay { selectedDay = -1 } else { selectedDay = dayViewModel.day } update(animated: true) } func sectionController(_ sectionController: ListBindingSectionController, didDeselectItemAt index: Int, viewModel: Any) {} func sectionController(_ sectionController: ListBindingSectionController, didHighlightItemAt index: Int, viewModel: Any) {} func sectionController(_ sectionController: ListBindingSectionController, didUnhighlightItemAt index: Int, viewModel: Any) {} @available(iOS 13.0, *) func sectionController(_ sectionController: ListBindingSectionController, contextMenuConfigurationForItemAt index: Int, point: CGPoint, viewModel: Any) -> UIContextMenuConfiguration? { return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in // Create an action for sharing let share = UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up")) { _ in // Show share sheet } // Create an action for copy let rename = UIAction(title: "Copy", image: UIImage(systemName: "doc.on.doc")) { _ in // Perform copy } // Create an action for delete with destructive attributes (highligh in red) let delete = UIAction(title: "Delete", image: UIImage(systemName: "trash"), attributes: .destructive) { _ in // Perform delete } // Create a UIMenu with all the actions as children return UIMenu(title: "", children: [share, rename, delete]) } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/PersonSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ @import IGListKit; @interface PersonSectionController : IGListSectionController @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/PersonSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "PersonSectionController.h" #import "PersonCell.h" #import "PersonModel.h" @implementation PersonSectionController { PersonModel *_person; } #pragma mark - IGListSectionController Overrides - (NSInteger)numberOfItems { return 1; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { const CGFloat width = self.collectionContext.containerSize.width; const CGFloat height = 74.0; return CGSizeMake(width, height); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { const Class cellClass = [PersonCell class]; PersonCell *cell = (PersonCell *)[self.collectionContext dequeueReusableCellOfClass:cellClass forSectionController:self atIndex:index]; cell.person = _person; return cell; } - (void)didUpdateToObject:(id)object { _person = (PersonModel *)object; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/PostSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ @import IGListKit; @interface PostSectionController : IGListSectionController @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/PostSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "PostSectionController.h" #import "CommentCell.h" #import "InteractiveCell.h" #import "PhotoCell.h" #import "Post.h" #import "UserInfoCell.h" static NSInteger cellsBeforeComments = 3; @implementation PostSectionController { Post *_post; } #pragma mark - IGListSectionController Overrides - (NSInteger)numberOfItems { return cellsBeforeComments + _post.comments.count; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { const CGFloat width = self.collectionContext.containerSize.width; CGFloat height; if (index == 0 || index == 2) { height = 41.0; } else if (index == 1) { height = width; // square } else { height = 25.0; } return CGSizeMake(width, height); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { Class cellClass; if (index == 0) { cellClass = [UserInfoCell class]; } else if (index == 1) { cellClass = [PhotoCell class]; } else if (index == 2) { cellClass = [InteractiveCell class]; } else { cellClass = [CommentCell class]; } id cell = [self.collectionContext dequeueReusableCellOfClass:cellClass forSectionController:self atIndex:index]; if ([cell isKindOfClass:[CommentCell class]]) { [(CommentCell *)cell setComment:_post.comments[index - cellsBeforeComments]]; } else if ([cell isKindOfClass:[UserInfoCell class]]) { [(UserInfoCell *)cell setName:_post.username]; } return cell; } - (void)didUpdateToObject:(id)object { _post = object; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/PostSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit // MARK: - ListSectionController // In IGListKit, section controllers manage a single type of data object // and are responsible for: // - Creating and configuring cells // - Handling cell selection // - Determining cell sizes // - Managing section-specific actions final class PostSectionController: ListSectionController { var post: Post? weak var delegate: PostSectionControllerDelegate? override init() { super.init() // Setting insets for the entire section self.inset = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) } // Determines the size of cells in this section // IGListKit calls this method to calculate cell dimensions override func sizeForItem(at index: Int) -> CGSize { // collectionContext is a bridge that connects the section controller // to the actual UICollectionView it exists within let width = collectionContext?.containerSize.width ?? 0 return CGSize(width: width, height: width + 200) } // Creates and configures a cell for this section // Similar to cellForRowAt in UICollectionViewDataSource override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell = collectionContext?.dequeueReusableCell(of: PostCell.self, for: self, at: index) as? PostCell, let post = post else { fatalError("Failed to dequeue PostCell") } cell.configure(with: post) cell.optionsButtonTapped = { [weak self] (button: UIButton) in guard let self = self, let post = self.post else { return } self.delegate?.postSectionController(self, didSelectOptionsFor: post, from: button) } return cell } // Handles selection of cells in this section override func didSelectItem(at index: Int) { guard let post = post else { return } print("Post ID:\(post.id) was tapped.") } // Called when the data object for this section controller is updated // This is where you store a reference to your model object override func didUpdate(to object: Any) { post = object as? Post } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/RemoveSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit protocol RemoveSectionControllerDelegate: AnyObject { func removeSectionControllerWantsRemoved(_ sectionController: RemoveSectionController) } final class RemoveSectionController: ListSectionController, RemoveCellDelegate { weak var delegate: RemoveSectionControllerDelegate? private var number: Int? override init() { super.init() inset = UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: RemoveCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = "Cell: \((number ?? 0) + 1)" cell.delegate = self return cell } override func didUpdate(to object: Any) { number = object as? Int } // MARK: RemoveCellDelegate func removeCellDidTapButton(_ cell: RemoveCell) { delegate?.removeSectionControllerWantsRemoved(self) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/ReorderableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class ReorderableSectionController: ListSectionController { private var object: String? override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: LabelCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object return cell } override func didUpdate(to object: Any) { self.object = String(describing: object) } override func canMoveItem(at index: Int) -> Bool { return true } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/SearchSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit protocol SearchSectionControllerDelegate: AnyObject { func searchSectionController(_ sectionController: SearchSectionController, didChangeText text: String) } final class SearchSectionController: ListSectionController, UISearchBarDelegate, ListScrollDelegate { weak var delegate: SearchSectionControllerDelegate? override init() { super.init() scrollDelegate = self } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 44) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: SearchCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.searchBar.delegate = self return cell } // MARK: UISearchBarDelegate func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { delegate?.searchSectionController(self, didChangeText: searchText) } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { delegate?.searchSectionController(self, didChangeText: searchBar.text!) } // MARK: ListScrollDelegate func listAdapter(_ listAdapter: ListAdapter, didScroll sectionController: ListSectionController) { if let searchBar = (collectionContext?.cellForItem(at: 0, sectionController: self) as? SearchCell)?.searchBar { searchBar.resignFirstResponder() } } func listAdapter(_ listAdapter: ListAdapter, willBeginDragging sectionController: ListSectionController) {} func listAdapter(_ listAdapter: ListAdapter, didEndDragging sectionController: ListSectionController, willDecelerate decelerate: Bool) {} } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/SelfSizingSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class SelfSizingSectionController: ListSectionController { private var model: SelectionModel! override init() { super.init() inset = UIEdgeInsets(top: 0, left: 0, bottom: 40, right: 0) minimumLineSpacing = 4 minimumInteritemSpacing = 4 } override func numberOfItems() -> Int { return model.options.count } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let text = model.options[index] let cell: UICollectionViewCell switch model.type { case .none: let manualCell: ManuallySelfSizingCell = collectionContext.dequeueReusableCell( for: self, at: index ) manualCell.text = text cell = manualCell case .fullWidth: let manualCell: FullWidthSelfSizingCell = collectionContext.dequeueReusableCell( for: self, at: index ) manualCell.text = text cell = manualCell case .nib: let nibCell: NibSelfSizingCell = collectionContext.dequeueReusableCell( withNibName: "NibSelfSizingCell", bundle: nil, for: self, at: index ) nibCell.contentLabel.text = text cell = nibCell } return cell } override func didUpdate(to object: Any) { self.model = object as? SelectionModel } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/StoryboardLabelSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit protocol StoryboardLabelSectionControllerDelegate: AnyObject { func removeSectionControllerWantsRemoved(_ sectionController: StoryboardLabelSectionController) } final class StoryboardLabelSectionController: ListSectionController { private var object: Person? weak var delegate: StoryboardLabelSectionControllerDelegate? override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: (self.object?.name.count)! * 7, height: (self.object?.name.count)! * 7) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: StoryboardCell = collectionContext.dequeueReusableCellFromStoryboard( withIdentifier: "cell", for: self, at: index) cell.text = object?.name return cell } override func didUpdate(to object: Any) { self.object = object as? Person } override func didSelectItem(at index: Int) { delegate?.removeSectionControllerWantsRemoved(self) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/UserSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class UserSectionController: ListSectionController { private var user: User? private let isReorderable: Bool required init(isReorderable: Bool = false) { self.isReorderable = isReorderable super.init() } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: DetailLabelCell = collectionContext.dequeueReusableCell( for: self, at: index ) cell.title = user?.name cell.detail = "@" + (user?.handle ?? "") return cell } override func didUpdate(to object: Any) { self.user = object as? User } override func canMoveItem(at index: Int) -> Bool { return isReorderable } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/ActivityComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit import SwiftUI final class ActivityComposableSectionController: ListSectionController, CompositionLayoutCapable, ListSupplementaryViewSource { private var activity: ActivityItem? override init() { super.init() supplementaryViewSource = self } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 60) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: UICollectionViewCell = collectionContext.dequeueReusableCell( for: self, at: index ) cell.contentConfiguration = UIHostingConfiguration(content: { HStack { Circle() .fill( LinearGradient(colors: [ .blue.opacity(0.3), .red.opacity(0.3)], startPoint: .topLeading, endPoint: .bottomTrailing) ) .frame(width: 44) .padding(16) Text(activity?.bodyText ?? "No body") Spacer() RoundedRectangle(cornerRadius: 10) .fill( RadialGradient(colors: [ .green.opacity(0.3), .yellow.opacity(0.3)], center: .center, startRadius: 5, endRadius: 75) ) .frame(width: 44, height: 44) .padding(16) } }) .margins(.all, 0) return cell } func supportedElementKinds() -> [String] { return [UICollectionView.elementKindSectionHeader, UICollectionView.elementKindSectionFooter] } func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView { let view: UICollectionViewListCell = collectionContext.dequeueReusableSupplementaryView( ofKind: elementKind, forSectionController: self, atIndex: index) view.contentConfiguration = UIHostingConfiguration(content: { switch elementKind { case UICollectionView.elementKindSectionHeader: Text("Activity Start") case UICollectionView.elementKindSectionFooter: Text("Activity End") default: EmptyView() } }) return view } func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize { if activity?.header != nil && elementKind == UICollectionView.elementKindSectionHeader { return CGSize(width: collectionContext.containerSize.width, height: 40) } if activity?.footer != nil && elementKind == UICollectionView.elementKindSectionFooter { return CGSize(width: collectionContext.containerSize.width, height: 40) } return .zero } override func didUpdate(to object: Any) { self.activity = object as? ActivityItem } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { var maxItemHeight: CGFloat = 0.0 var anyItemHasEstimatedHeight = false let itemCount: Int = numberOfItems() let items = (0.. 0.0 { group.interItemSpacing = NSCollectionLayoutSpacing.fixed(minimumInteritemSpacing) } let layoutSection = NSCollectionLayoutSection(group: group) if let suplementaryViewProvider = supplementaryViewSource { layoutSection.boundarySupplementaryItems = suplementaryViewProvider.supportedElementKinds().map { kind in let size = suplementaryViewProvider.sizeForSupplementaryView(ofKind: kind, at: 0) let layoutSize = NSCollectionLayoutSize(widthDimension: .absolute(size.width), heightDimension: .absolute(size.height)) let alignment: NSRectAlignment switch kind { case UICollectionView.elementKindSectionHeader: alignment = .top case UICollectionView.elementKindSectionFooter: alignment = .bottom default: alignment = .none } return NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSize, elementKind: kind, alignment: alignment) } } if inset != .zero { layoutSection.contentInsets = NSDirectionalEdgeInsets(top: inset.top, leading: inset.left, bottom: inset.bottom, trailing: inset.right) } if minimumLineSpacing > 0.0 { layoutSection.interGroupSpacing = minimumLineSpacing } return layoutSection } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/ExpandableComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit /// Like ExpandableSectionController, but supports UICollectionViewCompositionalLayout final class ExpandableComposableSectionController: ListSectionController, CompositionLayoutCapable { private var expanded = false private var object: String? override func sizeForItem(at index: Int) -> CGSize { // Size handled by cell return CGSize.zero } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CompositionLayoutCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object cell.expanded = expanded return cell } override func didUpdate(to object: Any) { self.object = object as? String } override func didSelectItem(at index: Int) { expanded = !expanded guard let cell = collectionContext.cellForItem(at: index, sectionController: self) as? CompositionLayoutCell else { return } cell.expanded = expanded UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: { self.collectionContext?.invalidateLayout(for: self) }) } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { let config = UICollectionLayoutListConfiguration(appearance: .plain) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/GridComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class GridComposableSectionController: ListSectionController, CompositionLayoutCapable { private var object: GridItem? override func numberOfItems() -> Int { return object?.itemCount ?? 0 } override func sizeForItem(at index: Int) -> CGSize { // Size handled by cell return CGSize.zero } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CompositionLayoutCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object?.items[index] ?? "undefined" cell.contentView.backgroundColor = object?.color return cell } override func didUpdate(to object: Any) { self.object = object as? GridItem } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { // Item let columnCount: CGFloat = 3 let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0 / columnCount), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2) // Group let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(1.0 / columnCount)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // Section return NSCollectionLayoutSection(group: group) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/HorizontalComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class HorizontalComposableSectionController: ListSectionController, CompositionLayoutCapable { private var object: HorizontalCardsSection? override func numberOfItems() -> Int { return object?.cardCount ?? 0 } override func sizeForItem(at index: Int) -> CGSize { // Size handled by cell return CGSize.zero } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CompositionLayoutCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = object?.items[index] ?? "undefined" cell.contentView.backgroundColor = UIColor.secondarySystemBackground cell.contentView.layer.cornerRadius = 8 return cell } override func didUpdate(to object: Any) { self.object = object as? HorizontalCardsSection } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { // Item let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16) // Group let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(160)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) // Section let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary return section } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/SelectionComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit import SwiftUI final class TextViewModel: ObservableObject { @Published var text: String @Published var extraPadding: Bool = false init(text: String) { self.text = text } } struct ExpandTextView: View { @ObservedObject var viewModel: TextViewModel var body: some View { Text(viewModel.text) .padding(viewModel.extraPadding ? 20 : 10) .border(.gray.opacity(0.2), width: 1) } } final class SelectionComposableSectionController: ListSectionController, CompositionLayoutCapable { private var viewModels: [TextViewModel] = [] override func numberOfItems() -> Int { return viewModels.count } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: 100, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: UICollectionViewCell = collectionContext.dequeueReusableCell( for: self, at: index ) let viewModel = viewModels[index] cell.contentConfiguration = UIHostingConfiguration(content: { ExpandTextView(viewModel: viewModel) }) .margins(.all, 0) return cell } override func didSelectItem(at index: Int) { withAnimation { viewModels[index].extraPadding.toggle() } } override func didUpdate(to object: Any) { guard let selection = object as? SelectionModel else { return } viewModels = selection.options.map { TextViewModel(text: $0) } } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { var maxItemHeight: CGFloat = 0.0 let itemCount: Int = numberOfItems() let items = (0.. 0.0 { group.interItemSpacing = NSCollectionLayoutSpacing.fixed(minimumInteritemSpacing) } let layoutSection = NSCollectionLayoutSection(group: group) if let suplementaryViewProvider = supplementaryViewSource { layoutSection.boundarySupplementaryItems = suplementaryViewProvider.supportedElementKinds().map { kind in let size = suplementaryViewProvider.sizeForSupplementaryView(ofKind: kind, at: 0) let layoutSize = NSCollectionLayoutSize(widthDimension: .absolute(size.width), heightDimension: .absolute(size.height)) let alignment: NSRectAlignment switch kind { case UICollectionView.elementKindSectionHeader: alignment = .top case UICollectionView.elementKindSectionFooter: alignment = .bottom default: alignment = .none } return NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSize, elementKind: kind, alignment: alignment) } } if inset != .zero { layoutSection.contentInsets = NSDirectionalEdgeInsets(top: inset.top, leading: inset.left, bottom: inset.bottom, trailing: inset.right) } if minimumLineSpacing > 0.0 { layoutSection.interGroupSpacing = minimumLineSpacing } return layoutSection } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/SwipeActionComposabelSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SwipeActionComposabelSectionController: ListSectionController, CompositionLayoutCapable { private var object: SwipeActionSection? private var items = ["1. Swipe to delete me", "2. Swipe to delete me", "3. Swipe to delete me"] override func numberOfItems() -> Int { return items.count } override func sizeForItem(at index: Int) -> CGSize { // Compositional layout doesn't request sizes per NSIndexPath return CGSize.zero } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: CompositionLayoutCell = collectionContext.dequeueReusableCell(for: self, at: index) cell.text = items[index] return cell } override func didUpdate(to object: Any) { self.object = object as? SwipeActionSection } func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { var config = UICollectionLayoutListConfiguration(appearance: .plain) config.trailingSwipeActionsConfigurationProvider = {[weak self] indexPath in // Sections should match, but just in case guard let self = self, indexPath.section == self.section else { return nil } return self.swipeActionFor(index: indexPath.item) } return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } // MARK: CompositionLayoutCapable private func swipeActionFor(index: Int) -> UISwipeActionsConfiguration? { let action = UIContextualAction(style: .destructive, title: "Delete") {[weak self] _, _, block in self?.deleteItem(index: index, block: block) } return UISwipeActionsConfiguration(actions: [action]) } private func deleteItem(index: Int, block: @escaping (Bool) -> Void) { self.collectionContext.performBatch(animated: true) {updates in self.items.remove(at: index) updates.delete(in: self, at: IndexSet(integer: index)) } completion: { completed in block(completed) } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/With Composable Layout/UserComposableSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class UserComposableSectionController: ListSectionController, CompositionLayoutCapable { private var user: User? override func sizeForItem(at index: Int) -> CGSize { // Size handled by cell return CGSize.zero } override func cellForItem(at index: Int) -> UICollectionViewCell { let cell: DetailLabelCell = collectionContext.dequeueReusableCell( for: self, at: index ) cell.title = user?.name cell.detail = "@" + (user?.handle ?? "") return cell } override func didUpdate(to object: Any) { self.user = object as? User } // MARK: CompositionLayoutCapable func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { let config = UICollectionLayoutListConfiguration(appearance: .plain) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/SectionControllers/WorkingRangeSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class WorkingRangeSectionController: ListSectionController, ListWorkingRangeDelegate { private var height: Int? private var downloadedImage: UIImage? private var task: URLSessionDataTask? private var urlString: String? { guard let height = height, let size = collectionContext?.containerSize else { return nil } let width = Int(size.width) return "https://unsplash.it/" + width.description + "/" + height.description } deinit { task?.cancel() } override init() { super.init() workingRangeDelegate = self } override func numberOfItems() -> Int { return 2 } override func sizeForItem(at index: Int) -> CGSize { let width: CGFloat = collectionContext?.containerSize.width ?? 0 let height: CGFloat = CGFloat(index == 0 ? 55 : (self.height ?? 0)) return CGSize(width: width, height: height) } override func cellForItem(at index: Int) -> UICollectionViewCell { let cellClass: UICollectionViewCell.Type = index == 0 ? LabelCell.self : ImageCell.self let cell = collectionContext.dequeueReusableCell(of: cellClass, for: self, at: index) if let cell = cell as? LabelCell { cell.text = urlString } else if let cell = cell as? ImageCell { cell.setImage(image: downloadedImage) } return cell } override func didUpdate(to object: Any) { self.height = object as? Int } // MARK: ListWorkingRangeDelegate func listAdapter(_ listAdapter: ListAdapter, sectionControllerWillEnterWorkingRange sectionController: ListSectionController) { guard downloadedImage == nil, task == nil, let urlString = urlString, let url = URL(string: urlString) else { return } print("Downloading image \(urlString) for section \(self.section)") task = URLSession.shared.dataTask(with: url) { data, _, error in guard let data = data, let image = UIImage(data: data) else { return print("Error downloading \(urlString): " + String(describing: error)) } DispatchQueue.main.async { self.downloadedImage = image if let cell = self.collectionContext?.cellForItem(at: 1, sectionController: self) as? ImageCell { cell.setImage(image: image) } } } task?.resume() } func listAdapter(_ listAdapter: ListAdapter, sectionControllerDidExitWorkingRange sectionController: ListSectionController) {} } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Storyboard/Demo.storyboard ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Systems/IncrementAnnouncer.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit @objc protocol IncrementListener: AnyObject { func didIncrement(announcer: IncrementAnnouncer, value: Int) } final class IncrementAnnouncer: NSObject { private var value: Int = 0 private let map: NSHashTable = NSHashTable.weakObjects() func addListener(listener: IncrementListener) { map.add(listener) } func increment() { value += 1 for listener in map.allObjects { listener.didIncrement(announcer: self, value: value) } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/AnnouncingDepsViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class AnnouncingDepsViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 1) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let data: [NSNumber] = Array(0..<20).map { NSNumber(value: $0) } let announcer = IncrementAnnouncer() override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self // disable prefetching so cells are configured as they come on screen if #available(iOS 10.0, *) { collectionView.isPrefetchingEnabled = false } navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(AnnouncingDepsViewController.onAdd)) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } @objc func onAdd() { announcer.increment() } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return ListeningSectionController(announcer: announcer) } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/CalendarViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class CalendarViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView( frame: .zero, collectionViewLayout: ListCollectionViewLayout(stickyHeaders: false, topContentInset: 0, stretchToEdge: false) ) var months = [Month]() override func viewDidLoad() { super.viewDidLoad() let date = Date() let currentMonth = Calendar.current.component(.month, from: date) let month = Month( name: DateFormatter().monthSymbols[currentMonth - 1], days: 30, appointments: [ 2: ["Hair"], 4: ["Nails"], 7: ["Doctor appt", "Pick up groceries"], 12: ["Call back the cable company", "Find a babysitter"], 13: ["Dinner at The Smith"], 17: ["Buy running shoes", "Buy a fitbit", "Start running"], 20: ["Call mom"], 21: ["Contribute to IGListKit"], 25: ["Interview"], 26: ["Quit running", "Buy ice cream"] ] ) months.append(month) view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return months } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return MonthSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/CompositionLayoutViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit /// Enables SectionControllers to return their own layout. In the future, we might want IGListKit /// to handle this, but for now, lets keep it simple. protocol CompositionLayoutCapable { func collectionViewSectionLayout(layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? } /// Like MixedDataViewController, but using UICollectionViewCompositionalLayout final class CompositionLayoutViewController: UIViewController, ListAdapterDataSource { private var collectionView: UICollectionView? private lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() private let data: [Any] = [ ActivityItem(bodyText: "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", header: "Activity Start"), ActivityItem(bodyText: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore."), ActivityItem(bodyText: "Excepteur sint occaecat cupidatat non proident."), ActivityItem(bodyText: "Dominus", footer: "Activity End"), SelectionModel(options: ["Leverage agile", "frameworks", "robust synopsis", "high level", "overviews", "Iterative approaches", "corporate strategy", "foster collaborative", "overall value", "proposition", "Organically grow", "holistic world view", "disruptive", "innovation", "workplace diversity", "empowerment"]), "Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.", GridItem(color: UIColor(red: 237 / 255.0, green: 73 / 255.0, blue: 86 / 255.0, alpha: 1), itemCount: 6), User(pk: 2, name: "Ryan Olson", handle: "ryanolsonk"), HorizontalCardsSection(cardCount: 10), SwipeActionSection(), "Praesent commodo cursus magna, vel scelerisque nisl consectetur et.", User(pk: 4, name: "Oliver Rickard", handle: "ocrickard"), HorizontalCardsSection(cardCount: 2), GridItem(color: UIColor(red: 56 / 255.0, green: 151 / 255.0, blue: 240 / 255.0, alpha: 1), itemCount: 5), "Nullam quis risus eget urna mollis ornare vel eu leo. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.", User(pk: 3, name: "Jesse Squires", handle: "jesse_squires"), GridItem(color: UIColor(red: 112 / 255.0, green: 192 / 255.0, blue: 80 / 255.0, alpha: 1), itemCount: 3), "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.", GridItem(color: UIColor(red: 163 / 255.0, green: 42 / 255.0, blue: 186 / 255.0, alpha: 1), itemCount: 7), User(pk: 1, name: "Ryan Nystrom", handle: "_ryannystrom") ] override func viewDidLoad() { super.viewDidLoad() // Layout let layout = UICollectionViewCompositionalLayout {[weak self] (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in guard let self = self else { return nil } let controller = self.adapter.sectionController(forSection: sectionIndex) guard let controller = controller as? CompositionLayoutCapable else { return nil } return controller.collectionViewSectionLayout(layoutEnvironment: layoutEnvironment) } // Collection View let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) self.collectionView = collectionView view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView?.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data.map { $0 as! ListDiffable } } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { switch object { case is String: return ExpandableComposableSectionController() case is GridItem: return GridComposableSectionController() case is User: return UserComposableSectionController() case is HorizontalCardsSection: return HorizontalComposableSectionController() case is SwipeActionSection: return SwipeActionComposabelSectionController() case is ActivityItem: return ActivityComposableSectionController() case is SelectionModel: return SelectionComposableSectionController() default: return ListSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/DemosViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class DemosViewController: UIViewController, ListAdapterDataSource { let horizontalInset = 16.0 lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let demos: [DemoItem] = [ DemoItem(name: "Tail Loading", imageName: "arrow.down.circle", controllerClass: LoadMoreViewController.self), DemoItem(name: "Search Autocomplete", imageName: "magnifyingglass", controllerClass: SearchViewController.self), DemoItem(name: "Mixed Data", imageName: "square.fill.text.grid.1x2", controllerClass: MixedDataViewController.self), DemoItem(name: "Nested Adapter", imageName: "curlybraces", controllerClass: NestedAdapterViewController.self), DemoItem(name: "Empty View", imageName: "exclamationmark.triangle", controllerClass: EmptyViewController.self), DemoItem(name: "Single Section Controller", imageName: "1.square", controllerClass: SingleSectionViewController.self), DemoItem(name: "Storyboard", imageName: "rectangle.on.rectangle", controllerClass: SingleSectionViewController.self, controllerIdentifier: "demo"), DemoItem(name: "Single Section Storyboard", imageName: "rectangle", controllerClass: SingleSectionStoryboardViewController.self, controllerIdentifier: "singleSectionDemo"), DemoItem(name: "Working Range", imageName: "arrow.left.and.right", controllerClass: WorkingRangeViewController.self), DemoItem(name: "Diff Algorithm", imageName: "function", controllerClass: DiffTableViewController.self), DemoItem(name: "Supplementary Views", imageName: "square.stack.3d.up", controllerClass: SupplementaryViewController.self), DemoItem(name: "Self-sizing cells", imageName: "brain", controllerClass: SelfSizingCellsViewController.self), DemoItem(name: "Display delegate", imageName: "megaphone", controllerClass: DisplayViewController.self), DemoItem(name: "Objc Demo", imageName: "c.square", controllerClass: ObjcDemoViewController.self), DemoItem(name: "Objc Generated Model Demo", imageName: "c.circle", controllerClass: ObjcGeneratedModelDemoViewController.self), DemoItem(name: "Calendar (auto diffing)", imageName: "calendar", controllerClass: CalendarViewController.self), DemoItem(name: "Dependency Injection", imageName: "syringe", controllerClass: AnnouncingDepsViewController.self), DemoItem(name: "Reorder Cells", imageName: "arrow.up.and.down.and.arrow.left.and.right", controllerClass: ReorderableViewController.self), DemoItem(name: "Compositional Layout", imageName: "square.stack", controllerClass: CompositionLayoutViewController.self), DemoItem(name: "Feed View", imageName: "camera.circle", controllerClass: FeedViewController.self) ] override func viewDidLoad() { super.viewDidLoad() title = "IGListKit" navigationController?.navigationBar.prefersLargeTitles = true collectionView.alwaysBounceVertical = true collectionView.backgroundColor = .groupedBackground collectionView.contentInset = UIEdgeInsets(top: 0, left: horizontalInset, bottom: 0, right: horizontalInset) view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self if splitViewController?.viewControllers.count ?? 0 < 2, let demoItem = demos.first { let viewController = demoItem.controllerClass.init() viewController.title = demoItem.name let navigationController = UINavigationController(rootViewController: viewController) navigationController.navigationBar.prefersLargeTitles = true splitViewController?.viewControllers.append(navigationController) } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.collectionViewLayout.invalidateLayout() collectionView.frame = view.bounds } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) guard let splitViewController = splitViewController else { return } if splitViewController.viewControllers.count > 1 { // When on iPad, this view controller is visible all the time, so on initial launch, select the first section if let firstSection = adapter.sectionController(forSection: 0) { firstSection.collectionContext.selectItem(at: 0, sectionController: firstSection, animated: false, scrollPosition: .top) firstSection.didSelectItem(at: 0) } } else { // On iPhone, deselect all cells when returning to this view controller (since we'll be coming back from a navigation pop) for sectionController in adapter.visibleSectionControllers() { sectionController.collectionContext.deselectItem(at: 0, sectionController: sectionController, animated: animated) // UIColletionView doesn't call the deselection delegate by design when manually deselected, so manually deselect here sectionController.didDeselectItem(at: 0) } } } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return demos } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return DemoSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/DiffTableViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class Person: ListDiffable { let pk: Int let name: String init(pk: Int, name: String) { self.pk = pk self.name = name } func diffIdentifier() -> NSObjectProtocol { return pk as NSNumber } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? Person else { return false } return self.name == object.name } } final class DiffTableViewController: UITableViewController { let oldPeople = [ Person(pk: 1, name: "Kevin"), Person(pk: 2, name: "Mike"), Person(pk: 3, name: "Ann"), Person(pk: 4, name: "Jane"), Person(pk: 5, name: "Philip"), Person(pk: 6, name: "Mona"), Person(pk: 7, name: "Tami"), Person(pk: 8, name: "Jesse"), Person(pk: 9, name: "Jaed") ] let newPeople = [ Person(pk: 2, name: "Mike"), Person(pk: 10, name: "Marne"), Person(pk: 5, name: "Philip"), Person(pk: 1, name: "Kevin"), Person(pk: 3, name: "Ryan"), Person(pk: 8, name: "Jesse"), Person(pk: 7, name: "Tami"), Person(pk: 4, name: "Jane"), Person(pk: 9, name: "Chen") ] lazy var people: [Person] = { return self.oldPeople }() var usingOldPeople = true override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(DiffTableViewController.onDiff)) tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } @objc func onDiff() { let from = people let to = usingOldPeople ? newPeople : oldPeople usingOldPeople = !usingOldPeople people = to let result = ListDiffPaths(fromSection: 0, toSection: 0, oldArray: from, newArray: to, option: .equality).forBatchUpdates() tableView.beginUpdates() tableView.deleteRows(at: result.deletes, with: .fade) tableView.insertRows(at: result.inserts, with: .fade) result.moves.forEach { tableView.moveRow(at: $0.from, to: $0.to) } tableView.endUpdates() } // MARK: UITableViewDataSource override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return people.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.textLabel?.text = people[indexPath.row].name return cell } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/DisplayViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class DisplayViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return [1, 2, 3, 4, 5, 6] as [NSNumber] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return DisplaySectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/EmptyViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class EmptyViewController: UIViewController, ListAdapterDataSource, RemoveSectionControllerDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) collectionView.backgroundColor = UIColor.secondaryBackground return collectionView }() let emptyLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 label.textAlignment = .center label.text = "No more data!" label.backgroundColor = .clear return label }() var tally = 4 var data = [1, 2, 3, 4] override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(EmptyViewController.onAdd)) view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } @objc func onAdd() { data.append(tally + 1) tally += 1 adapter.performUpdates(animated: true, completion: nil) } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let sectionController = RemoveSectionController() sectionController.delegate = self return sectionController } func emptyView(for listAdapter: ListAdapter) -> UIView? { return emptyLabel } // MARK: RemoveSectionControllerDelegate func removeSectionControllerWantsRemoved(_ sectionController: RemoveSectionController) { let section = adapter.section(for: sectionController) guard let object = adapter.object(atSection: section) as? Int, let index = data.firstIndex(of: object) else { return } data.remove(at: index) adapter.performUpdates(animated: true, completion: nil) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/FeedViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class FeedViewController: UIViewController, ListAdapterDataSource { // MARK: - Properties private lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.backgroundColor = .systemBackground collectionView.alwaysBounceVertical = true return collectionView }() private var posts: [Post] = [] private var isLoading = false private var shouldShowLoadingCell = false // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() setupUI() setupAdapter() loadInitialData() } // MARK: - Setup private func setupUI() { title = "Feed View" view.backgroundColor = .systemBackground navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .refresh, target: self, action: #selector(refreshFeed) ) view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) } private func setupAdapter() { adapter.collectionView = collectionView adapter.scrollViewDelegate = self adapter.dataSource = self } // MARK: - Data Loading private func loadInitialData() { isLoading = true APIService.shared.resetPagination() posts = [] shouldShowLoadingCell = true adapter.performUpdates(animated: true) APIService.shared.fetchPosts { [weak self] newPosts in guard let self = self else { return } self.posts = newPosts self.isLoading = false self.adapter.performUpdates(animated: true) } } private func loadMoreData() { guard !isLoading else { return } isLoading = true shouldShowLoadingCell = true adapter.performUpdates(animated: true) APIService.shared.fetchPosts { [weak self] newPosts in guard let self = self else { return } // Append new posts to existing posts self.posts.append(contentsOf: newPosts) self.isLoading = false // If no new posts were fetched, hide the loading cell if newPosts.isEmpty { self.shouldShowLoadingCell = false } self.adapter.performUpdates(animated: true) } } @objc private func refreshFeed() { loadInitialData() } // MARK: - ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { var objects: [ListDiffable] = posts if shouldShowLoadingCell { objects.append(LoadingCellModel()) } return objects } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if object is Post { let sectionController = PostSectionController() sectionController.delegate = self return sectionController } else { return LoadingSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { let emptyView = UIView() let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = "No posts available" label.textAlignment = .center label.textColor = .gray emptyView.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor), label.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor) ]) return emptyView } } // MARK: - UIScrollViewDelegate extension FeedViewController: UIScrollViewDelegate { func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { let distance = scrollView.contentSize.height - (targetContentOffset.pointee.y + scrollView.bounds.height) if !isLoading && distance < 200 && !posts.isEmpty { loadMoreData() } } } // MARK: - PostSectionControllerDelegate extension FeedViewController: PostSectionControllerDelegate { func postSectionController(_ sectionController: PostSectionController, didSelectOptionsFor post: Post, from sourceView: UIView) { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let deleteAction = UIAlertAction(title: "Delete", style: .destructive) { [weak self] _ in self?.deletePost(post) } let reportAction = UIAlertAction(title: "Report", style: .default) { _ in print("Reported post: \(post.id)") } let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) alertController.addAction(deleteAction) alertController.addAction(reportAction) alertController.addAction(cancelAction) // Configure for iPad if let popoverController = alertController.popoverPresentationController { // If we have a specific source view (like a button), use it popoverController.sourceView = sourceView popoverController.sourceRect = sourceView.bounds } present(alertController, animated: true, completion: nil) } func postSectionController(_ sectionController: PostSectionController, didRequestDeleteFor post: Post) { deletePost(post) } private func deletePost(_ post: Post) { if let index = posts.firstIndex(where: { $0.id == post.id }) { posts.remove(at: index) adapter.performUpdates(animated: true) } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/IGListKitExamples-Bridging-Header.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "ObjcDemoViewController.h" #import "ObjcGeneratedModelDemoViewController.h" ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/LoadMoreViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class LoadMoreViewController: UIViewController, ListAdapterDataSource, UIScrollViewDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) lazy var items: [Int] = { // Load more on iPad in order for the content size to be bigger than its screen. UIDevice.current.userInterfaceIdiom == .pad ? Array(0...30) : Array(0...15) }() var loading = false let spinToken = "spinner" override func viewDidLoad() { super.viewDidLoad() collectionView.alwaysBounceVertical = true view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self adapter.scrollViewDelegate = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds collectionView.collectionViewLayout.invalidateLayout() } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { var objects = items as [ListDiffable] if loading { objects.append(spinToken as ListDiffable) } return objects } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if let obj = object as? String, obj == spinToken { return spinnerSectionController() } else { return LabelSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: UIScrollViewDelegate func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { let distance = scrollView.contentSize.height - (targetContentOffset.pointee.y + scrollView.bounds.height) if !loading && distance < 200 { loading = true adapter.performUpdates(animated: true, completion: nil) DispatchQueue.global(qos: .default).async { // fake background loading task sleep(2) DispatchQueue.main.async { self.loading = false let itemCount = self.items.count self.items.append(contentsOf: Array(itemCount.. [ListDiffable] { guard selectedClass != nil else { return data.map { $0 as! ListDiffable } } return data.filter { type(of: $0) == selectedClass! } .map { $0 as! ListDiffable } } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { switch object { case is String: return ExpandableSectionController() case is GridItem: return GridSectionController(isReorderable: true) default: return UserSectionController(isReorderable: true) } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: - ListAdapterMoveDelegate func listAdapter(_ listAdapter: ListAdapter, move object: Any, from previousObjects: [Any], to objects: [Any]) { data = objects } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/NestedAdapterViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class NestedAdapterViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let data: [Any] = [ "Ridiculus Elit Tellus Purus Aenean", "Condimentum Sollicitudin Adipiscing", 14, "Ligula Ipsum Tristique Parturient Euismod", "Purus Dapibus Vulputate", 6, "Tellus Nibh Ipsum Inceptos", 2 ] override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as! [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if object is Int { return HorizontalSectionController() } else { return LabelSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/ObjcDemoViewController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface ObjcDemoViewController : UIViewController @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/ObjcDemoViewController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "ObjcDemoViewController.h" @import IGListKit; #import "Post.h" #import "PostSectionController.h" @interface ObjcDemoViewController () @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, copy) NSArray *data; @end @implementation ObjcDemoViewController - (void)viewDidLoad { [super viewDidLoad]; self.data = @[ [[Post alloc] initWithUsername:@"userA" comments:@[ @"Luminous triangle", @"Awesome", @"Super clean", @"Stunning shot", ]], [[Post alloc] initWithUsername:@"userB" comments:@[ @"The simplicity here is superb", @"thanks!", @"That's always so kind of you!", @"I think you might like this", ]], [[Post alloc] initWithUsername:@"userC" comments:@[ @"So good", ]], [[Post alloc] initWithUsername:@"userD" comments:@[ @"hope she might like it.", @"I love it." ]], ]; self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.view addSubview:self.collectionView]; self.adapter = [[IGListAdapter alloc] initWithUpdater:[[IGListAdapterUpdater alloc] init] viewController:self]; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.collectionView.frame = self.view.bounds; } #pragma mark - IGListAdapterDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.data; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [PostSectionController new]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/ObjcGeneratedModelDemoViewController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface ObjcGeneratedModelDemoViewController : UIViewController @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/ObjcGeneratedModelDemoViewController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "ObjcGeneratedModelDemoViewController.h" @import IGListKit; #import "PersonModel.h" #import "PersonSectionController.h" @interface ObjcGeneratedModelDemoViewController () @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, copy) NSArray *persons; @end @implementation ObjcGeneratedModelDemoViewController - (void)viewDidLoad { [super viewDidLoad]; self.persons = @[[[PersonModel alloc] initWithFirstName:@"Ryan" lastName:@"Nystrom" uniqueId:@"1"], [[PersonModel alloc] initWithFirstName:@"Jesse" lastName:@"Squires" uniqueId:@"2"], [[PersonModel alloc] initWithFirstName:@"Markus" lastName:@"Emrich" uniqueId:@"3"], [[PersonModel alloc] initWithFirstName:@"James" lastName:@"Sherlock" uniqueId:@"4"], [[PersonModel alloc] initWithFirstName:@"Bofei" lastName:@"Zhu" uniqueId:@"5"], [[PersonModel alloc] initWithFirstName:@"Valeriy" lastName:@"Van" uniqueId:@"6"], [[PersonModel alloc] initWithFirstName:@"Hesham" lastName:@"Salman" uniqueId:@"7"], [[PersonModel alloc] initWithFirstName:@"Bas" lastName:@"Broek" uniqueId:@"8"], [[PersonModel alloc] initWithFirstName:@"Andrew" lastName:@"Monshizadeh" uniqueId:@"9"], [[PersonModel alloc] initWithFirstName:@"Adlai" lastName:@"Holler" uniqueId:@"10"], [[PersonModel alloc] initWithFirstName:@"Ben" lastName:@"Asher" uniqueId:@"11"], [[PersonModel alloc] initWithFirstName:@"Jake" lastName:@"Lin" uniqueId:@"12"], [[PersonModel alloc] initWithFirstName:@"Jeff" lastName:@"Bailey" uniqueId:@"13"], [[PersonModel alloc] initWithFirstName:@"Daniel" lastName:@"Alamo" uniqueId:@"14"], [[PersonModel alloc] initWithFirstName:@"Viktor" lastName:@"Gardart" uniqueId:@"15"], [[PersonModel alloc] initWithFirstName:@"Anton" lastName:@"Sotkov" uniqueId:@"16"], [[PersonModel alloc] initWithFirstName:@"Guilherme" lastName:@"Rambo" uniqueId:@"17"], [[PersonModel alloc] initWithFirstName:@"Rizwan" lastName:@"Ibrahim" uniqueId:@"18"], [[PersonModel alloc] initWithFirstName:@"Ayush" lastName:@"Saraswat" uniqueId:@"19"], [[PersonModel alloc] initWithFirstName:@"Dustin" lastName:@"Shahidehpour" uniqueId:@"20"], ]; self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.view addSubview:self.collectionView]; self.adapter = [[IGListAdapter alloc] initWithUpdater:[[IGListAdapterUpdater alloc] init] viewController:self]; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.collectionView.frame = self.view.bounds; } #pragma mark - IGListAdapterDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.persons; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [PersonSectionController new]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/ReorderableViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class ReorderableViewController: UIViewController, ListAdapterDataSource, ListAdapterMoveDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) var data = Array(0..<20).map { "Cell: \($0 + 1)" } // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() if #available(iOS 9.0, *) { let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ReorderableViewController.handleLongGesture(gesture:))) collectionView.addGestureRecognizer(longPressGesture) } view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self if #available(iOS 9.0, *) { adapter.moveDelegate = self } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: - Interactive Reordering @available(iOS 9.0, *) @objc func handleLongGesture(gesture: UILongPressGestureRecognizer) { switch gesture.state { case .began: let touchLocation = gesture.location(in: self.collectionView) guard let selectedIndexPath = collectionView.indexPathForItem(at: touchLocation) else { break } collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) case .changed: if let view = gesture.view { let position = gesture.location(in: view) collectionView.updateInteractiveMovementTargetPosition(position) } case .ended: collectionView.endInteractiveMovement() default: collectionView.cancelInteractiveMovement() } } // MARK: - ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return ReorderableSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: - ListAdapterMoveDelegate func listAdapter(_ listAdapter: ListAdapter, move object: Any, from previousObjects: [Any], to objects: [Any]) { guard let objects = objects as? [String] else { return } data = objects } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/SearchViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SearchViewController: UIViewController, ListAdapterDataSource, SearchSectionControllerDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) lazy var words: [String] = { // swiftlint:disable:next let str = "Humblebrag skateboard tacos viral small batch blue bottle, schlitz fingerstache etsy squid. Listicle tote bag helvetica XOXO literally, meggings cardigan kickstarter roof party deep v selvage scenester venmo truffaut. You probably haven't heard of them fanny pack austin next level 3 wolf moon. Everyday carry offal brunch 8-bit, keytar banjo pinterest leggings hashtag wolf raw denim butcher. Single-origin coffee try-hard echo park neutra, cornhole banh mi meh austin readymade tacos taxidermy pug tattooed. Cold-pressed +1 ethical, four loko cardigan meh forage YOLO health goth sriracha kale chips. Mumblecore cardigan humblebrag, lo-fi typewriter truffaut leggings health goth." var unique = Set() var words = [String]() let range = str.startIndex ..< str.endIndex str.enumerateSubstrings(in: range, options: .byWords) { (substring, _, _, _) in guard let substring = substring else { return } if !unique.contains(substring) { unique.insert(substring) words.append(substring) } } return words }() var filterString = "" let searchToken: NSNumber = 42 override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { guard filterString != "" else { return [searchToken] + words.map { $0 as ListDiffable } } return [searchToken] + words.filter { $0.lowercased().contains(filterString.lowercased()) }.map { $0 as ListDiffable } } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if let obj = object as? NSNumber, obj == searchToken { let sectionController = SearchSectionController() sectionController.delegate = self return sectionController } else { return LabelSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: SearchSectionControllerDelegate func searchSectionController(_ sectionController: SearchSectionController, didChangeText text: String) { filterString = text adapter.performUpdates(animated: true, completion: nil) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/SelfSizingCellsViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SelfSizingCellsViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.estimatedItemSize = CGSize(width: 100, height: 40) let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = UIColor(red: 0.831_372_549, green: 0.945_098_039, blue: 0.964_705_882, alpha: 1) return collectionView }() let data = [ SelectionModel(options: ["Leverage agile", "frameworks", "robust synopsis", "high level", "overviews", "Iterative approaches", "corporate strategy", "foster collaborative", "overall value", "proposition", "Organically grow", "holistic world view", "disruptive", "innovation", "workplace diversity", "empowerment"]), SelectionModel(options: ["Bring to the table", "win-win", "survival", "strategies", "proactive domination", "At the end of the day", "going forward", "a new normal", "evolved", "generation X", "runway heading", "streamlined", "cloud solution", "User generated", "content", "in real-time", "multiple touchpoints", "offshoring"], type: .nib), SelectionModel(options: ["Aenean lacinia bibendum nulla sed consectetur. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras mattis consectetur purus sit amet fermentum.", "Donec sed odio dui. Donec id elit non mi porta gravida at eget metus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed posuere consectetur est at lobortis. Cras justo odio, dapibus ac facilisis in, egestas eget quam.", "Sed posuere consectetur est at lobortis. Nullam quis risus eget urna mollis ornare vel eu leo. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum."], type: .fullWidth) ] override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return SelfSizingSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/SingleSectionStoryboardViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SingleSectionStoryboardViewController: UIViewController, ListAdapterDataSource, ListSingleSectionControllerDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() @IBOutlet weak var collectionView: UICollectionView! let data = Array(0..<20) // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() adapter.collectionView = collectionView adapter.dataSource = self } // MARK: - ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let configureBlock = { (item: Any, cell: UICollectionViewCell) in guard let cell = cell as? StoryboardCell, let number = item as? Int else { return } cell.text = "Cell: \(number + 1)" } let sizeBlock = { (_: Any, context: ListCollectionContext?) -> CGSize in guard let context = context else { return .zero } return CGSize(width: context.containerSize.width, height: 44) } let sectionController = ListSingleSectionController(storyboardCellIdentifier: "cell", configureBlock: configureBlock, sizeBlock: sizeBlock) sectionController.selectionDelegate = self return sectionController } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: - ListSingleSectionControllerDelegate func didSelect(_ sectionController: ListSingleSectionController, with object: Any) { let section = adapter.section(for: sectionController) + 1 let alert = UIAlertController(title: "Section \(section) was selected \u{1F389}", message: nil, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil)) present(alert, animated: true, completion: nil) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/SingleSectionViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SingleSectionViewController: UIViewController, ListAdapterDataSource, ListSingleSectionControllerDelegate { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let data = Array(0..<20) // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: - ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let configureBlock = { (item: Any, cell: UICollectionViewCell) in guard let cell = cell as? NibCell, let number = item as? Int else { return } cell.text = "Cell: \(number + 1)" } let sizeBlock = { (_: Any, context: ListCollectionContext?) -> CGSize in guard let context = context else { return CGSize() } return CGSize(width: context.containerSize.width, height: 44) } let sectionController = ListSingleSectionController(nibName: NibCell.nibName, bundle: nil, configureBlock: configureBlock, sizeBlock: sizeBlock) sectionController.selectionDelegate = self return sectionController } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } // MARK: - ListSingleSectionControllerDelegate func didSelect(_ sectionController: ListSingleSectionController, with object: Any) { let section = adapter.section(for: sectionController) + 1 let alert = UIAlertController( title: "Section \(section) was selected \u{1F389}", message: "Cell Object: " + String(describing: object), preferredStyle: .alert) alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil)) present(alert, animated: true, completion: nil) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/StoryboardViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class StoryboardViewController: UIViewController, ListAdapterDataSource, StoryboardLabelSectionControllerDelegate { @IBOutlet weak var collectionView: UICollectionView! lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() lazy var people = [ Person(pk: 1, name: "Littlefinger"), Person(pk: 2, name: "Tommen Baratheon"), Person(pk: 3, name: "Roose Bolton"), Person(pk: 4, name: "Brienne of Tarth"), Person(pk: 5, name: "Bronn"), Person(pk: 6, name: "Gilly"), Person(pk: 7, name: "Theon Greyjoy"), Person(pk: 8, name: "Jaqen H'ghar"), Person(pk: 9, name: "Cersei Lannister"), Person(pk: 10, name: "Jaime Lannister"), Person(pk: 11, name: "Tyrion Lannister"), Person(pk: 12, name: "Melisandre"), Person(pk: 13, name: "Missandei"), Person(pk: 14, name: "Jorah Mormont"), Person(pk: 15, name: "Khal Moro"), Person(pk: 16, name: "Daario Naharis"), Person(pk: 17, name: "Jon Snow"), Person(pk: 18, name: "Arya Stark"), Person(pk: 19, name: "Bran Stark"), Person(pk: 20, name: "Sansa Stark"), Person(pk: 21, name: "Daenerys Targaryen"), Person(pk: 22, name: "Samwell Tarly"), Person(pk: 23, name: "Tormund"), Person(pk: 24, name: "Margaery Tyrell"), Person(pk: 25, name: "Varys"), Person(pk: 26, name: "Renly Baratheon"), Person(pk: 27, name: "Joffrey Baratheon"), Person(pk: 28, name: "Stannis Baratheon"), Person(pk: 29, name: "Hodor"), Person(pk: 30, name: "Tywin Lannister"), Person(pk: 31, name: "The Hound"), Person(pk: 32, name: "Ramsay Bolton") ] override func viewDidLoad() { super.viewDidLoad() adapter.collectionView = collectionView adapter.dataSource = self } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return people } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let sectionController = StoryboardLabelSectionController() sectionController.delegate = self return sectionController } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } func removeSectionControllerWantsRemoved(_ sectionController: StoryboardLabelSectionController) { let section = adapter.section(for: sectionController) people.remove(at: Int(section)) adapter.performUpdates(animated: true) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/SupplementaryViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class SupplementaryViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let feedItems = [ FeedItem(pk: 1, user: User(pk: 100, name: "Jesse", handle: "jesse_squires"), comments: ["You rock!", "Hmm you sure about that?"]), FeedItem(pk: 2, user: User(pk: 101, name: "Ryan", handle: "_ryannystrom"), comments: ["lgtm", "lol", "Let's try it!"]), FeedItem(pk: 3, user: User(pk: 102, name: "Ann", handle: "abaum"), comments: ["Good luck!"]), FeedItem(pk: 4, user: User(pk: 103, name: "Phil", handle: "phil"), comments: ["yoooooooo", "What's the eta?"]) ] override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return feedItems } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return FeedItemSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/ViewControllers/WorkingRangeViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class WorkingRangeViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 2) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let data: [Int] = { var set = Set() // only use unique values while set.count < 20 { set.insert( Int(arc4random_uniform(200)) + 200 ) } return Array(set) }() override func viewDidLoad() { super.viewDidLoad() view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return WorkingRangeSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/CalendarDayCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class CalendarDayCell: UICollectionViewCell { lazy fileprivate var label: UILabel = { let view = UILabel() view.backgroundColor = .clear view.textAlignment = .center view.font = .boldSystemFont(ofSize: 16) view.layer.borderWidth = 2 view.clipsToBounds = true self.contentView.addSubview(view) return view }() lazy fileprivate var dotsLabel: UILabel = { let view = UILabel() view.backgroundColor = .clear view.textAlignment = .center view.textColor = .red view.font = .boldSystemFont(ofSize: 30) self.contentView.addSubview(view) return view }() var text: String? { get { return label.text } set { label.text = newValue } } var dots: String? { get { return dotsLabel.text } set { dotsLabel.text = newValue } } override func layoutSubviews() { super.layoutSubviews() let bounds = contentView.bounds let half = bounds.height / 2 label.frame = bounds label.layer.cornerRadius = half dotsLabel.frame = CGRect(x: 0, y: half - 10, width: bounds.width, height: half) } } extension CalendarDayCell: ListBindable { func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? DayViewModel else { return } label.text = viewModel.day.description label.layer.borderColor = viewModel.today ? UIColor.red.cgColor : UIColor.clear.cgColor label.backgroundColor = viewModel.selected ? UIColor.red.withAlphaComponent(0.3) : UIColor.clear var dots = "" for _ in 0.. @interface CommentCell : UICollectionViewCell @property (nonatomic, copy) NSString *comment; @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/CommentCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "CommentCell.h" @interface CommentCell () @property (nonatomic, strong) UILabel *commentLabel; @end @implementation CommentCell - (instancetype)init { if (self = [super init]) { [self setupSubviews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupSubviews]; } return self; } - (void)setupSubviews { self.commentLabel = [[UILabel alloc] init]; self.commentLabel.textColor = [UIColor colorWithRed:0.59 green:0.59 blue:0.57 alpha:1.0]; self.commentLabel.textAlignment = NSTextAlignmentLeft; self.commentLabel.font = [UIFont systemFontOfSize:11]; [self.contentView addSubview:self.commentLabel]; } - (void)layoutSubviews { [super layoutSubviews]; CGFloat left = 8.0; CGRect bounds = self.contentView.bounds; self.commentLabel.frame = CGRectMake(left, 0, bounds.size.width - left * 2.0, bounds.size.height); } - (void)setComment:(NSString *)comment { _comment = [comment copy]; self.commentLabel.text = _comment; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/CompositionLayoutCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit /// A cell that works with composition layout. /// 1. It overrides -sizeThatFits for cell sizing /// 2. Unlike LabelCell, it doesn't need to add separators, since that's taken care of by the layout class CompositionLayoutCell: UICollectionViewCell { private let insets = UIEdgeInsets(top: 12, left: 16, bottom: 12, right: 16) override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(label) contentView.backgroundColor = UIColor.systemBackground } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() label.frame = bounds.inset(by: insets) } override func prepareForReuse() { super.prepareForReuse() contentView.backgroundColor = UIColor.systemBackground contentView.layer.cornerRadius = 0 } // MARK: Label private let label: UILabel = { let label = UILabel() label.backgroundColor = .clear label.font = UIFont.systemFont(ofSize: 18) return label }() var text: String? { get { return label.text } set { label.text = newValue } } var expanded: Bool = false { didSet { label.numberOfLines = expanded ? 0 : 1 } } override func sizeThatFits(_ size: CGSize) -> CGSize { let labelMaxSize = CGRect(origin: .zero, size: size).inset(by: insets).size let labelSize = label.sizeThatFits(labelMaxSize) return CGSize(width: labelSize.width + insets.left + insets.right, height: labelSize.height + insets.top + insets.bottom) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/DetailLabelCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class DetailLabelCell: UICollectionViewCell { fileprivate let padding: CGFloat = 15.0 lazy private var titleLabel: UILabel = { let view = UILabel() view.backgroundColor = .clear view.textAlignment = .left view.font = .systemFont(ofSize: 17) view.textColor = UIColor.titleLabel self.contentView.addSubview(view) return view }() lazy private var detailLabel: UILabel = { let view = UILabel() view.backgroundColor = .clear view.textAlignment = .right view.font = .systemFont(ofSize: 17) view.textColor = UIColor.detailLabel self.contentView.addSubview(view) return view }() var title: String? { get { return titleLabel.text } set { titleLabel.text = newValue } } var detail: String? { get { return detailLabel.text } set { detailLabel.text = newValue } } override func layoutSubviews() { super.layoutSubviews() let frame = contentView.bounds.insetBy(dx: padding, dy: 0) titleLabel.frame = frame detailLabel.frame = frame } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/EmbeddedCollectionViewCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class EmbeddedCollectionViewCell: UICollectionViewCell { lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.backgroundColor = .clear view.alwaysBounceVertical = false view.alwaysBounceHorizontal = true self.contentView.addSubview(view) return view }() override func layoutSubviews() { super.layoutSubviews() collectionView.frame = contentView.frame } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/FullWidthSelfSizingCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class FullWidthSelfSizingCell: UICollectionViewCell { private let label: UILabel = { let label = UILabel() label.backgroundColor = UIColor.red.withAlphaComponent(0.1) label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false return label }() var text: String? { get { return label.text } set { label.text = newValue } } override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = UIColor.background contentView.addSubview(label) NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: contentView, attribute: .trailing, relatedBy: .equal, toItem: label, attribute: .trailing, multiplier: 1, constant: 15).isActive = true } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { setNeedsLayout() layoutIfNeeded() let size = contentView.systemLayoutSizeFitting(layoutAttributes.size) var newFrame = layoutAttributes.frame // note: don't change the width newFrame.size.height = ceil(size.height) layoutAttributes.frame = newFrame return layoutAttributes } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/ImageCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class ImageCell: UICollectionViewCell { fileprivate let imageView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill view.clipsToBounds = true view.backgroundColor = UIColor.secondaryBackground return view }() fileprivate let activityView: UIActivityIndicatorView = { let view = UIActivityIndicatorView() view.style = UIActivityIndicatorView.defaultStyle view.startAnimating() return view }() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(imageView) contentView.addSubview(activityView) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() let bounds = contentView.bounds activityView.center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0) imageView.frame = bounds } func setImage(image: UIImage?) { imageView.image = image if image != nil { activityView.stopAnimating() } else { activityView.startAnimating() } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/InteractiveCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface InteractiveCell : UICollectionViewCell @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/InteractiveCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "InteractiveCell.h" @interface InteractiveCell () @property (nonatomic, strong) UIButton *likeButton; @property (nonatomic, strong) UIButton *commentButton; @property (nonatomic, strong) UIButton *shareButton; @property (nonatomic, strong) CALayer *separator; @end @implementation InteractiveCell - (instancetype)init { if (self = [super init]) { [self setupSubviews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupSubviews]; } return self; } - (void)setupSubviews { UIColor *buttonTitleColor; if (@available(iOS 13.0, *)) { buttonTitleColor = [UIColor labelColor]; } else { buttonTitleColor = [UIColor colorWithRed:28/255.0 green:30/255.0 blue:28/255.0 alpha:1.0]; } UIFont *titleFont = [UIFont systemFontOfSize:12.0]; self.likeButton = [[UIButton alloc] init]; [self.likeButton setTitle:@"Like" forState:UIControlStateNormal]; [self.likeButton setTitleColor:buttonTitleColor forState:UIControlStateNormal]; [self.likeButton.titleLabel setFont:titleFont]; [self.likeButton sizeToFit]; [self.contentView addSubview:self.likeButton]; self.commentButton = [[UIButton alloc] init]; [self.commentButton setTitle:@"Comment" forState:UIControlStateNormal]; [self.commentButton setTitleColor:buttonTitleColor forState:UIControlStateNormal]; [self.commentButton.titleLabel setFont:titleFont]; [self.commentButton sizeToFit]; [self.contentView addSubview:self.commentButton]; self.shareButton = [[UIButton alloc] init]; [self.shareButton setTitle:@"Share" forState:UIControlStateNormal]; [self.shareButton setTitleColor:buttonTitleColor forState:UIControlStateNormal]; [self.shareButton.titleLabel setFont:titleFont]; [self.shareButton sizeToFit]; [self.contentView addSubview:self.shareButton]; self.separator = [[CALayer alloc] init]; self.separator.backgroundColor = [UIColor colorWithRed:200/255.0 green:199/255.0 blue:204/255.0 alpha:1].CGColor; [self.contentView.layer addSublayer:self.separator]; } - (void)layoutSubviews { [super layoutSubviews]; CGRect bounds = self.contentView.bounds; CGFloat leftPadding = 8.0; self.likeButton.frame = CGRectMake(leftPadding, 0, CGRectGetWidth(self.likeButton.frame), bounds.size.height); self.commentButton.frame = CGRectMake(leftPadding + CGRectGetMaxX(self.likeButton.frame), 0, CGRectGetWidth(self.commentButton.frame), bounds.size.height); self.shareButton.frame = CGRectMake(leftPadding + CGRectGetMaxX(self.commentButton.frame), 0, CGRectGetWidth(self.shareButton.frame), bounds.size.height); CGFloat height = 0.5; self.separator.frame = CGRectMake(leftPadding, bounds.size.height - height, bounds.size.width - leftPadding, height); } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/LabelCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class LabelCell: UICollectionViewCell { enum Style { case `default` case grouped } fileprivate static let insets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15) fileprivate static let font = UIFont.systemFont(ofSize: 18) fileprivate static let symbolFont = UIFont.boldSystemFont(ofSize: 21) fileprivate static let cornerRadius = 12.0 static var singleLineHeight: CGFloat { return font.lineHeight + insets.top + insets.bottom } static func textHeight(_ text: String, width: CGFloat) -> CGFloat { let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude) let attributes = [ NSAttributedString.Key.font: font ] let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin] let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes, context: nil) return ceil(bounds.height) + insets.top + insets.bottom } fileprivate let label: UILabel = { let label = UILabel() label.backgroundColor = .clear label.numberOfLines = 0 label.font = LabelCell.font return label }() let separator: CALayer = { let layer = CALayer() layer.backgroundColor = UIColor.defaultSeparator.cgColor return layer }() let imageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = .titleLabel imageView.contentMode = .scaleAspectFit return imageView }() let disclosureImageView: UIImageView = { let configuration = UIImage.SymbolConfiguration(pointSize: 17, weight: .bold, scale: .small) let chevronImage = UIImage(systemName: "chevron.right", withConfiguration: configuration) let imageView = UIImageView(image: chevronImage) imageView.tintColor = .defaultSeparator imageView.isHidden = true return imageView }() let backdropView: UIView = { let view = UIView() view.layer.cornerCurve = .continuous return view }() var text: String? { get { return label.text } set { label.text = newValue } } var imageName: String? { didSet { guard let imageName else { return } let configuration = UIImage.SymbolConfiguration(font: LabelCell.symbolFont, scale: .default) let image = UIImage(systemName: imageName, withConfiguration: configuration) imageView.image = image setNeedsLayout() } } var style: Style = .default { didSet { backdropView.backgroundColor = backdropColor setHighlighted(isSelected) } } var isTopCell = false var isBottomCell = false var backdropColor: UIColor { (style == .grouped) ? .secondaryGroupedBackground : .background } override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = .clear contentView.addSubview(backdropView) contentView.addSubview(label) contentView.layer.addSublayer(separator) contentView.addSubview(disclosureImageView) contentView.addSubview(imageView) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() let bounds = contentView.bounds backdropView.frame = contentView.bounds let hasImage = imageName?.count ?? 0 > 0 let imageCenterX = (hasImage ? LabelCell.insets.left + 15 : 0) + safeAreaInsets.left if hasImage { imageView.sizeToFit() imageView.frame.origin = CGPoint(x: imageCenterX - imageView.bounds.midX, y: bounds.midY - imageView.bounds.midY) } disclosureImageView.frame.origin = CGPoint(x: bounds.width - (LabelCell.insets.right + disclosureImageView.bounds.width + safeAreaInsets.right), y: bounds.midY - disclosureImageView.bounds.midY) let labelX = hasImage ? imageCenterX + 25 : (LabelCell.insets.left + safeAreaInsets.left) label.frame = CGRect(x: labelX, y: LabelCell.insets.top, width: bounds.width - (labelX - disclosureImageView.frame.minX), height: bounds.height - (LabelCell.insets.top + LabelCell.insets.bottom)) let separatorHeight: CGFloat = 1.0 / (window?.screen.nativeScale ?? 2.0) let left = label.frame.minX separator.frame = CGRect(x: left, y: bounds.height - separatorHeight, width: bounds.width - left, height: separatorHeight) if style != .grouped { return } if isBottomCell { separator.isHidden = true } let layer = backdropView.layer layer.cornerRadius = LabelCell.cornerRadius if isTopCell { layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } else if isBottomCell { layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] } else { layer.maskedCorners = [] } } override var isHighlighted: Bool { didSet { setHighlighted(isHighlighted) } } override var isSelected: Bool { didSet { setHighlighted(isSelected) } } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) separator.backgroundColor = UIColor.defaultSeparator.cgColor } private func setHighlighted(_ highlighted: Bool) { let color = highlighted ? UIColor.gray.withAlphaComponent(0.3) : backdropColor backdropView.backgroundColor = color } } extension LabelCell: ListBindable { func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? String else { return } label.text = viewModel } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/LoadingCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class LoadingCell: UICollectionViewCell { private let activityIndicator: UIActivityIndicatorView = { let indicator = UIActivityIndicatorView(style: .medium) indicator.translatesAutoresizingMaskIntoConstraints = false indicator.color = .darkGray indicator.hidesWhenStopped = false return indicator }() override init(frame: CGRect) { super.init(frame: frame) setupViews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupViews() { contentView.addSubview(activityIndicator) NSLayoutConstraint.activate([ activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) ]) activityIndicator.startAnimating() } override func prepareForReuse() { super.prepareForReuse() activityIndicator.startAnimating() } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/ManuallySelfSizingCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class ManuallySelfSizingCell: UICollectionViewCell { private let label: UILabel = { let label = UILabel() label.backgroundColor = UIColor.green.withAlphaComponent(0.1) label.numberOfLines = 1 label.translatesAutoresizingMaskIntoConstraints = false return label }() var text: String? { get { return label.text } set { label.text = newValue } } override init(frame: CGRect) { super.init(frame: frame) contentView.backgroundColor = UIColor.background contentView.addSubview(label) NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: contentView, attribute: .top, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: label, attribute: .leading, relatedBy: .equal, toItem: contentView, attribute: .leading, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1, constant: 15).isActive = true NSLayoutConstraint(item: contentView, attribute: .trailing, relatedBy: .equal, toItem: label, attribute: .trailing, multiplier: 1, constant: 15).isActive = true } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { setNeedsLayout() layoutIfNeeded() let size = contentView.systemLayoutSizeFitting(layoutAttributes.size) var newFrame = layoutAttributes.frame newFrame.size.width = ceil(size.width) newFrame.size.height = ceil(size.height) layoutAttributes.frame = newFrame return layoutAttributes } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/MonthTitleCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class MonthTitleCell: UICollectionViewCell { fileprivate lazy var label: UILabel = { let view = UILabel() view.backgroundColor = .clear view.textAlignment = .center view.textColor = UIColor(white: 0.7, alpha: 1) view.font = .boldSystemFont(ofSize: 13) self.contentView.addSubview(view) return view }() var text: String? { get { return label.text } set { label.text = newValue } } override func layoutSubviews() { super.layoutSubviews() label.frame = contentView.bounds } } extension MonthTitleCell: ListBindable { func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? MonthTitleViewModel else { return } label.text = viewModel.name.uppercased() } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/NibCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class NibCell: UICollectionViewCell { static let nibName = "NibCell" @IBOutlet private var textLabel: UILabel! var text: String? { didSet { textLabel.text = text } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/NibCell.xib ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/NibSelfSizingCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class NibSelfSizingCell: UICollectionViewCell { @IBOutlet weak var contentLabel: UILabel! private var content: String? { get { return contentLabel.text } set { contentLabel.text = newValue } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/NibSelfSizingCell.xib ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/PersonCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class PersonModel; @interface PersonCell : UICollectionViewCell @property (nonatomic, copy) PersonModel *person; @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/PersonCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "PersonCell.h" #import "PersonModel.h" @interface PersonCell () @property (nonatomic, strong) UIView *avatarView; @property (nonatomic, strong) UILabel *nameLabel; @property (nonatomic, strong) UIView *separatorView; @property (nonatomic, assign) CGFloat separatorHeight; @end @implementation PersonCell - (instancetype)init { if (self = [super init]) { [self setupSubviews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupSubviews]; } return self; } - (void)setupSubviews { self.avatarView = [[UIView alloc] init]; self.avatarView.backgroundColor = [UIColor colorWithWhite:0.85 alpha:1.0]; [self.contentView addSubview:self.avatarView]; self.nameLabel = [[UILabel alloc] init]; self.nameLabel.textAlignment = NSTextAlignmentLeft; [self.contentView addSubview:self.nameLabel]; self.separatorView = [[UIView alloc] init]; [self.contentView addSubview:self.separatorView]; if (@available(iOS 13.0, *)) { self.nameLabel.textColor = [UIColor labelColor]; self.separatorView.backgroundColor = [UIColor separatorColor]; } else { self.nameLabel.textColor = [UIColor darkTextColor]; self.separatorView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.0]; } self.separatorHeight = (1 / [UIScreen mainScreen].scale); } - (void)layoutSubviews { [super layoutSubviews]; const CGFloat outerInset = 10; const CGRect bounds = self.contentView.bounds; const CGRect insetBounds = CGRectInset(bounds, outerInset, outerInset); const CGFloat avatarViewWidth = insetBounds.size.height; const CGRect avatarViewFrame = CGRectMake(outerInset, outerInset, avatarViewWidth, avatarViewWidth); if (!CGRectEqualToRect(avatarViewFrame, self.avatarView.frame)) { self.avatarView.layer.cornerRadius = round(avatarViewWidth / 2.0); self.avatarView.layer.masksToBounds = YES; self.avatarView.frame = avatarViewFrame; } const CGFloat avatarLabelInset = 8; self.nameLabel.frame = CGRectMake(CGRectGetMaxX(avatarViewFrame) + avatarLabelInset, outerInset, CGRectGetWidth(insetBounds) - avatarViewWidth - avatarLabelInset * 2, CGRectGetHeight(insetBounds)); self.separatorView.frame = CGRectMake(0, CGRectGetHeight(bounds) - self.separatorHeight, CGRectGetWidth(bounds), self.separatorHeight); } static NSAttributedString *AttributedStringForPerson(PersonModel *person) { NSMutableAttributedString *string = [NSMutableAttributedString new]; [string appendAttributedString:[[NSAttributedString alloc] initWithString:person.firstName attributes:@{NSFontAttributeName:[UIFont boldSystemFontOfSize:15.0]}]]; [string appendAttributedString:[[NSAttributedString alloc] initWithString:@" " attributes:@{NSFontAttributeName:[UIFont boldSystemFontOfSize:15.0]}]]; [string appendAttributedString:[[NSAttributedString alloc] initWithString:person.lastName attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15.0]}]]; return string; } - (void)setPerson:(PersonModel *)person { _person = [person copy]; self.nameLabel.attributedText = AttributedStringForPerson(person); } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/PhotoCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface PhotoCell : UICollectionViewCell @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/PhotoCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "PhotoCell.h" @interface PhotoCell () @property (nonatomic, strong) UIView *view; @end @implementation PhotoCell - (instancetype)init { if (self = [super init]) { [self setupSubviews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupSubviews]; } return self; } - (void)setupSubviews { self.view = [[UIView alloc] init]; self.view.backgroundColor = [UIColor colorWithRed:4/255.0 green:170/255.0 blue:166/255.0 alpha:1.0]; [self.contentView addSubview:self.view]; } - (void)layoutSubviews { [super layoutSubviews]; self.view.frame = self.contentView.frame; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/PostCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit // MARK: - UICollectionViewCell // In IGListKit, cells are regular UICollectionViewCells // There's no special base class - IGListKit works with standard UIKit components final class PostCell: UICollectionViewCell { // MARK: - UI Components private let headerView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let avatarImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = 20 imageView.clipsToBounds = true imageView.backgroundColor = .systemGray5 return imageView }() private let usernameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.boldSystemFont(ofSize: 15) return label }() private let optionsButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(systemName: "ellipsis"), for: .normal) button.tintColor = .darkGray return button }() private let postImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.backgroundColor = .systemGray6 return imageView }() private let actionStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.spacing = 10 return stackView }() private let likeButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(systemName: "heart"), for: .normal) button.tintColor = .label return button }() private let commentButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(systemName: "message"), for: .normal) button.tintColor = .label return button }() private let shareButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setImage(UIImage(systemName: "paperplane"), for: .normal) button.tintColor = .label return button }() private let likesLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.boldSystemFont(ofSize: 14) return label }() private let captionStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.spacing = 5 return stackView }() private let captionUsernameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.boldSystemFont(ofSize: 14) label.setContentHuggingPriority(.defaultHigh, for: .horizontal) return label }() private let captionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 14) label.numberOfLines = 3 return label }() private let timestampLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 12) label.textColor = .gray return label }() // MARK: - Properties // Closure for handling the options button tap // This allows the section controller to respond to UI events in the cell var optionsButtonTapped: ((UIButton) -> Void)? // MARK: - Initialization override init(frame: CGRect) { super.init(frame: frame) setupViews() setupActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // Clean up cell for reuse - important for efficient cell recycling override func prepareForReuse() { super.prepareForReuse() avatarImageView.image = nil postImageView.image = nil usernameLabel.text = nil captionUsernameLabel.text = nil captionLabel.text = nil likesLabel.text = nil timestampLabel.text = nil } // MARK: - Setup private func setupViews() { optionsButton.accessibilityIdentifier = "optionsButton" contentView.backgroundColor = .systemBackground // Add subviews contentView.addSubview(headerView) headerView.addSubview(avatarImageView) headerView.addSubview(usernameLabel) headerView.addSubview(optionsButton) contentView.addSubview(postImageView) contentView.addSubview(actionStackView) actionStackView.addArrangedSubview(likeButton) actionStackView.addArrangedSubview(commentButton) actionStackView.addArrangedSubview(shareButton) contentView.addSubview(likesLabel) contentView.addSubview(captionStackView) captionStackView.addArrangedSubview(captionUsernameLabel) captionStackView.addArrangedSubview(captionLabel) contentView.addSubview(timestampLabel) // Layout constraints NSLayoutConstraint.activate([ // Header view headerView.topAnchor.constraint(equalTo: contentView.topAnchor), headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), headerView.heightAnchor.constraint(equalToConstant: 50), avatarImageView.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 10), avatarImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), avatarImageView.widthAnchor.constraint(equalToConstant: 40), avatarImageView.heightAnchor.constraint(equalToConstant: 40), usernameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 10), usernameLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), optionsButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -10), optionsButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), optionsButton.widthAnchor.constraint(equalToConstant: 30), optionsButton.heightAnchor.constraint(equalToConstant: 30), // Post image postImageView.topAnchor.constraint(equalTo: headerView.bottomAnchor), postImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), postImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), postImageView.heightAnchor.constraint(equalTo: contentView.widthAnchor), // Square aspect ratio // Action buttons actionStackView.topAnchor.constraint(equalTo: postImageView.bottomAnchor, constant: 8), actionStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), likeButton.widthAnchor.constraint(equalToConstant: 30), likeButton.heightAnchor.constraint(equalToConstant: 30), commentButton.widthAnchor.constraint(equalToConstant: 30), commentButton.heightAnchor.constraint(equalToConstant: 30), shareButton.widthAnchor.constraint(equalToConstant: 30), shareButton.heightAnchor.constraint(equalToConstant: 30), // Likes likesLabel.topAnchor.constraint(equalTo: actionStackView.bottomAnchor, constant: 5), likesLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), // Caption captionStackView.topAnchor.constraint(equalTo: likesLabel.bottomAnchor, constant: 5), captionStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), captionStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), // Timestamp timestampLabel.topAnchor.constraint(equalTo: captionStackView.bottomAnchor, constant: 5), timestampLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), timestampLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10) ]) } private func setupActions() { optionsButton.addTarget(self, action: #selector(handleOptionsButtonTap), for: .touchUpInside) } // MARK: - Actions @objc private func handleOptionsButtonTap() { optionsButtonTapped?(optionsButton) } // MARK: - Configuration // Configure the cell with data from a Post model // In IGListKit, cells are configured by their section controllers func configure(with post: Post) { usernameLabel.text = post.username captionUsernameLabel.text = post.username captionLabel.text = post.description likesLabel.text = "\(post.likes) likes" // Format timestamp let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full timestampLabel.text = formatter.localizedString(for: post.timeStamp, relativeTo: Date()) if let avatarURL = post.userAvatarURL { URLSession.shared.dataTask(with: avatarURL) { [weak self] data, _, _ in if let data = data, let image = UIImage(data: data) { DispatchQueue.main.async { self?.avatarImageView.image = image } } }.resume() } if let imageURL = post.imageURL { URLSession.shared.dataTask(with: imageURL) { [weak self] data, _, _ in if let data = data, let image = UIImage(data: data) { DispatchQueue.main.async { self?.postImageView.image = image } } }.resume() } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/RemoveCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit protocol RemoveCellDelegate: AnyObject { func removeCellDidTapButton(_ cell: RemoveCell) } final class RemoveCell: UICollectionViewCell { weak var delegate: RemoveCellDelegate? private lazy var label: UILabel = { let label = UILabel() label.backgroundColor = .clear self.contentView.addSubview(label) return label }() fileprivate lazy var button: UIButton = { let button = UIButton(type: .custom) button.setTitle("Remove", for: UIControl.State()) button.setTitleColor(.blue, for: UIControl.State()) button.backgroundColor = .clear button.addTarget(self, action: #selector(RemoveCell.onButton(_:)), for: .touchUpInside) self.contentView.addSubview(button) return button }() var text: String? { get { return label.text } set { label.text = newValue } } override func layoutSubviews() { super.layoutSubviews() contentView.backgroundColor = UIColor.background let bounds = contentView.bounds let divide = bounds.divided(atDistance: 100, from: .maxXEdge) label.frame = divide.slice.insetBy(dx: 15, dy: 0) button.frame = divide.remainder } @objc func onButton(_ button: UIButton) { delegate?.removeCellDidTapButton(self) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/SearchCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class SearchCell: UICollectionViewCell { lazy var searchBar: UISearchBar = { let view = UISearchBar() if #available(iOS 26.0, *) { view.searchBarStyle = .minimal } self.contentView.addSubview(view) return view }() override func layoutSubviews() { super.layoutSubviews() searchBar.frame = contentView.bounds } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/SpinnerCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit func spinnerSectionController() -> ListSingleSectionController { let configureBlock = { (_: Any, cell: SpinnerCell) in cell.activityIndicator.startAnimating() } let sizeBlock = { (_: Any, context: ListCollectionContext?) -> CGSize in guard let context = context else { return .zero } return CGSize(width: context.containerSize.width, height: 100) } return ListSingleSectionController(configure: configureBlock, size: sizeBlock) } final class SpinnerCell: UICollectionViewCell { lazy var activityIndicator: UIActivityIndicatorView = { let view = UIActivityIndicatorView() view.style = UIActivityIndicatorView.defaultStyle self.contentView.addSubview(view) return view }() override func layoutSubviews() { super.layoutSubviews() let bounds = contentView.bounds activityIndicator.center = CGPoint(x: bounds.midX, y: bounds.midY) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/StoryboardCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class StoryboardCell: UICollectionViewCell { @IBOutlet private weak var textLabel: UILabel! var text: String? { get { return textLabel.text } set { textLabel.text = newValue } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserFooterView.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class UserFooterView: UICollectionViewCell { @IBOutlet private weak var commentsCountLabel: UILabel! var commentsCount: String? { get { return commentsCountLabel.text } set { commentsCountLabel.text = newValue } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserFooterView.xib ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserHeaderView.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class UserHeaderView: UICollectionViewCell { @IBOutlet private weak var nameLabel: UILabel! @IBOutlet private weak var handleLabel: UILabel! var name: String? { get { return nameLabel.text } set { nameLabel.text = newValue } } var handle: String? { get { return handleLabel.text } set { handleLabel.text = newValue } } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserHeaderView.xib ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserInfoCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface UserInfoCell : UICollectionViewCell @property (nonatomic, copy) NSString *name; @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples/Views/UserInfoCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UserInfoCell.h" @interface UserInfoCell () @property (nonatomic, strong) UIView *avatarView; @property (nonatomic, strong) UILabel *nameLabel; @end @implementation UserInfoCell - (instancetype)init { if (self = [super init]) { [self setupSubviews]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupSubviews]; } return self; } - (void)setupSubviews { self.avatarView = [[UIView alloc] init]; self.avatarView.backgroundColor = [UIColor colorWithRed:210/255.0 green:65/255.0 blue:64/255.0 alpha:1.0]; [self.contentView addSubview:self.avatarView]; self.nameLabel = [[UILabel alloc] init]; self.nameLabel.font = [UIFont boldSystemFontOfSize:15]; self.nameLabel.textAlignment = NSTextAlignmentLeft; [self.contentView addSubview:self.nameLabel]; } - (void)layoutSubviews { [super layoutSubviews]; CGRect bounds = self.contentView.bounds; CGFloat avatarViewWidth = 25.0; CGFloat avatarTopSpace = (CGRectGetHeight(bounds) - avatarViewWidth) / 2.0; CGFloat avatarLeftSpace = 8.0; self.avatarView.frame = CGRectMake(avatarLeftSpace, avatarTopSpace, avatarViewWidth, avatarViewWidth); self.avatarView.layer.cornerRadius = MIN(CGRectGetHeight(self.avatarView.frame), CGRectGetWidth(self.avatarView.frame)) / 2.0; self.avatarView.layer.masksToBounds = YES; self.nameLabel.frame = CGRectMake(CGRectGetMaxX(self.avatarView.frame) + 8.0, CGRectGetMinY(self.avatarView.frame), CGRectGetWidth(bounds) - CGRectGetMaxX(self.avatarView.frame) - 8.0 * 2, CGRectGetHeight(self.avatarView.frame)); } - (void)setName:(NSString *)name { _name = [name copy]; self.nameLabel.text = _name; } @end ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/DemosViewControllerUITests.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest final class DemosViewControllerUITests: UITestCase { func test_whenSelectingTailLoading_thatTailLoadingDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Tail Loading") } func test_whenSelectingSearchAutocomplete_thatSearchAutocompleteDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Search Autocomplete") } func test_whenSelectingMixedData_thatMixedDataDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Mixed Data") } func test_whenSelectingNestedAdapter_thatNestedAdapterDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Nested Adapter") } func test_whenSelectingEmptyView_thatEmptyViewDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Empty View") } func test_whenSelectingSingleSectionController_thatSingleSectionControllerScreenIsPresented() { enterAndAssertScreen(withTitle: "Single Section Controller") } func test_whenSelectingStoryboard_thatStoryboardDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Storyboard") } func test_whenSelectingSingleSectionStoryboard_thatSingleSectionStoryboardDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Single Section Storyboard") } func test_whenSelectingWorkingRange_thatWorkingRangeDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Working Range") } func test_whenSelectingDiffAlgorithm_thatDiffAlgorithmDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Diff Algorithm") } func test_whenSelectingSupplementaryViews_thatSupplementaryViewsDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Supplementary Views") } func test_whenSelectingSelfSizingCells_thatSelfSizingCellsDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Self-sizing cells") } func test_whenSelectingDisplayDelegate_thatDisplayDelegateDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Display delegate") } func test_whenSelectingObjcDemo_thatObjcDemoDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Objc Demo") } func test_whenSelectingObjcGeneratedModelDemo_thatObjcGeneratedModelDemoDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Objc Generated Model Demo") } func test_whenSelectingCalendarDemo_thatCalendarDemoDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Calendar (auto diffing)") } func test_whenSelectingDependencyInjection_thatDependencyInjectionDemoDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Dependency Injection") } func test_whenSelectingRecorderCells_thatReorderCellsDemoDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Reorder Cells") } func test_whenSelectingFeedView_thatFeedViewDetailScreenIsPresented() { enterAndAssertScreen(withTitle: "Feed View") } private func enterAndAssertScreen(withTitle title: String) { let app = XCUIApplication() app.activate() let cell = app.collectionViews.cells.staticTexts[title] scrollToElement(cell) XCTAssertTrue(cell.exists, "Couldn’t find demo named “\(title)”") cell.tap() let exactBar = app.navigationBars[title] let compactBar = app.navigationBars[title.replacingOccurrences(of: " ", with: "")] waitToAppear(element: exactBar, timeout: 5) if !exactBar.exists { waitToAppear(element: compactBar, timeout: 5) } XCTAssertTrue( exactBar.exists || compactBar.exists, "Expected a navigation bar titled “\(title)” (or its compact form) to appear" ) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/FeedViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest final class FeedViewControllerUITests: UITestCase { // MARK: - Short-cuts private var app: XCUIApplication { XCUIApplication() } /// The collection-view that shows the FeedViewController's feed. private var feed: XCUIElement { let collections = app.collectionViews guard collections.count > 1 else { return collections.firstMatch } for idx in 0.. 0) } func test_whenRefreshing_newContentIsLoaded() { waitToAppear(element: feed.cells.element(boundBy: 0), timeout: 5) app.navigationBars["Feed View"].buttons["Refresh"].tap() waitToAppear(element: feed, timeout: 5) XCTAssertTrue(feed.exists) } func test_whenScrollingToBottom_loadMoreIndicatorAppears() { waitToAppear(element: feed.cells.element(boundBy: 0), timeout: 5) (0..<3).forEach { _ in feed.swipeUp() } XCTAssertTrue(feed.cells.count > 0) } func test_whenTappingOptionsButton_actionSheetAppears() { let firstPost = firstVisiblePost() locateOptionsButton(in: firstPost).tap() let sheet = app.sheets.firstMatch waitToAppear(element: sheet, timeout: 2) XCTAssertTrue(sheet.buttons["Delete"].exists) XCTAssertTrue(sheet.buttons["Report"].exists) app.tap() } func test_whenDeletingPost_postIsRemoved() { let firstPost = firstVisiblePost() let caption = firstPost.staticTexts.firstMatch.label XCTAssertFalse(caption.isEmpty, "Post should contain a visible caption") locateOptionsButton(in: firstPost).tap() waitToAppear(element: app.sheets.firstMatch, timeout: 2) app.sheets.firstMatch.buttons["Delete"].tap() let deletedCell = feed.cells .containing(.staticText, identifier: caption) .firstMatch XCTAssertTrue( waitUntilGone(element: deletedCell, timeout: 5), "Cell with caption “\(caption)” should disappear after deletion" ) } // MARK: - Helpers private func enterFeedViewControllerDetailScreen() { let demo = app.collectionViews.staticTexts["Feed View"] scrollToElement(demo, in: app.collectionViews) XCTAssertTrue(demo.exists) demo.tap() XCTAssertTrue(app.navigationBars["Feed View"].exists) } @discardableResult private func waitUntilGone(element: XCUIElement, timeout: TimeInterval) -> Bool { let gone = NSPredicate(format: "exists == false") let exp = expectation(for: gone, evaluatedWith: element, handler: nil) return XCTWaiter().wait(for: [exp], timeout: timeout) == .completed } private func firstVisiblePost() -> XCUIElement { let post = feed.cells.element(boundBy: 0) waitToAppear(element: post, timeout: 5) return post } /// Finds the trailing “…” button in a post, whatever Apple calls it this week. private func locateOptionsButton(in post: XCUIElement) -> XCUIElement { let predicate = NSPredicate( format: "identifier == 'optionsButton' || " + "label CONTAINS[c] 'ellipsis' || " + "label CONTAINS[c] 'more'" ) for container in [post, feed, app] { let btn = container.descendants(matching: .any) .matching(predicate) .firstMatch if btn.waitForExistence(timeout: 2) { return btn } } fatalError("Options button should appear somewhere on screen") } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/LoadMoreViewControllerUITests.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest final class LoadMoreViewControllerUITests: UITestCase { func test_whenScrollingToTheBottom_thatNewItemsAreLoaded() { let collectionViews = XCUIApplication().collectionViews collectionViews.cells.staticTexts["Tail Loading"].tap() // Swipe up until the last item in the list is on-screen var numberOfTries = 0 let lastElem = collectionViews.cells.staticTexts["15"] while !lastElem.isHittable { collectionViews.element.swipeUp() numberOfTries += 1 if numberOfTries >= 10 { break } } // Wait for the following item to be loaded asynchronously let newlyLoadedElement = collectionViews.cells.staticTexts["16"] waitToAppear(element: newlyLoadedElement, timeout: 30.0) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/MixedDataViewControllerUITests.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest final class MixedDataViewControllerUITests: UITestCase { override func setUp() { super.setUp() enterMixedDataDetailScreen() } func test_whenSelectingAll_thatAllSectionTypesArePresent() { mixedDataNavigationBarElement().buttons["All"].tap() XCTAssertTrue(expandableSectionElement().exists) XCTAssertTrue(userSectionElement().exists) XCTAssertTrue(gridSectionElement().exists) } func test_whenSelectingColors_thatOnlyGridSectionsArePresent() { mixedDataNavigationBarElement().buttons["Colors"].tap() XCTAssertFalse(expandableSectionElement().exists) XCTAssertFalse(userSectionElement().exists) XCTAssertTrue(gridSectionElement().exists) } func test_whenSelectingText_thatOnlyExpandableSectionsArePresent() { mixedDataNavigationBarElement().buttons["Text"].tap() XCTAssertTrue(expandableSectionElement().exists) XCTAssertFalse(userSectionElement().exists) XCTAssertFalse(gridSectionElement().exists) } func test_whenSelectingUsers_thatOnlyUserSectionsArePresent() { mixedDataNavigationBarElement().buttons["Users"].tap() XCTAssertFalse(expandableSectionElement().exists) XCTAssertTrue(userSectionElement().exists) XCTAssertFalse(gridSectionElement().exists) } func test_whenExpandingExpandableSection_thatHeightIsIncreased() { mixedDataNavigationBarElement().buttons["Text"].tap() let expandableSection = expandableSectionElement() let collapsedFrame = expandableSection.frame // Expand expandableSection.tap() let expandedFrame = expandableSection.frame XCTAssertTrue(expandedFrame.size.height > collapsedFrame.size.height) } func test_whenCollapsingExpandableSection_thatHeightIsDecreased() { mixedDataNavigationBarElement().buttons["Text"].tap() let expandableSection = expandableSectionElement() // Expand expandableSection.tap() let expandedFrame = expandableSection.frame // Collapse expandableSection.tap() let collapsedFrame = expandableSection.frame XCTAssertTrue(collapsedFrame.size.height < expandedFrame.size.height) } private func expandableSectionElement() -> XCUIElement { return XCUIApplication().collectionViews.cells.staticTexts.element(matching: NSPredicate(format: "label == %@", "Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.")) } private func userSectionElement() -> XCUIElement { return XCUIApplication().collectionViews.cells.staticTexts["@ryanolsonk"] } private func gridSectionElement() -> XCUIElement { return XCUIApplication().collectionViews.cells.staticTexts["1"] } private func mixedDataNavigationBarElement() -> XCUIElement { return XCUIApplication().navigationBars["Mixed Data"] } private func enterMixedDataDetailScreen() { XCUIApplication().collectionViews.cells.staticTexts["Mixed Data"].tap() } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/SearchViewControllerUITests.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest final class SearchViewControllerUITests: UITestCase { var collectionViews: XCUIElementQuery! override func setUp() { super.setUp() collectionViews = XCUIApplication().collectionViews collectionViews.cells.staticTexts["Search Autocomplete"].tap() } func test_whenLoading_thatSomeResultsAreShown() { let tacos = collectionViews.cells.staticTexts["tacos"] let small = collectionViews.cells.staticTexts["small"] XCTAssertTrue(tacos.exists) XCTAssertTrue(small.exists) } func test_whenSearchingForText_thatResultsGetFiltered() { let searchField = collectionViews.searchFields.element searchField.tap() searchField.typeText("tac") let tacos = collectionViews.cells.staticTexts["tacos"] let small = collectionViews.cells.staticTexts["small"] XCTAssertTrue(tacos.exists) XCTAssertFalse(small.exists) } func test_whenClearingText_thatResultsFilterIsRemoved() { let searchField = collectionViews.searchFields.element searchField.tap() searchField.typeText("tac") searchField.buttons.element.tap() let tacos = collectionViews.cells.staticTexts["tacos"] let small = collectionViews.cells.staticTexts["small"] XCTAssertTrue(tacos.exists) XCTAssertTrue(small.exists) } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples-UITests/UITestCase.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest class UITestCase: XCTestCase { override func setUp() { super.setUp() continueAfterFailure = false XCUIApplication().launch() } // Adapted from http://masilotti.com/xctest-helpers/ internal func waitToAppear(element: XCUIElement, timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { waitToAppear(elements: [element], timeout: timeout, file: file, line: line) } internal func waitToAppear(elements: [XCUIElement], timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { waitTo(appear: true, elements: elements, timeout: timeout, file: file, line: line) } internal func waitToDisappear(element: XCUIElement, timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { waitToDisappear(elements: [element], timeout: timeout, file: file, line: line) } internal func waitToDisappear(elements: [XCUIElement], timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { waitTo(appear: false, elements: elements, timeout: timeout, file: file, line: line) } internal func waitTo(appear: Bool, elements: [XCUIElement], timeout: TimeInterval = 2, file: String = #file, line: UInt = #line) { let existsPredicate = NSPredicate(format: "exists == \(appear)") elements.forEach { element in expectation(for: existsPredicate, evaluatedWith: element, handler: nil) } waitForExpectations(timeout: timeout) { error in if error != nil { let message = "Failed to \(appear ? "" : "not ")find element(s) after \(timeout) seconds." self.recordFailure(withDescription: message, inFile: file, atLine: Int(line), expected: true) } } } } // MARK: - Helpers added for multiple collection-views (iPad split-view) private extension XCUIElementQuery { /// The list we want to scroll in our UI-tests. /// • On iPhone there is only one collection-view, so this is that one. /// • On iPad split-view there are two; `firstMatch` is always the master list. var primary: XCUIElement { firstMatch } } /// Scrolls until `element` is hittable or `maxSwipes` is reached. internal func scrollToElement(_ element: XCUIElement, in scrollView: XCUIElementQuery = XCUIApplication().collectionViews, maxSwipes: Int = 15) { var swipes = 0 while !element.isHittable && swipes < maxSwipes { scrollView.primary.swipeUp() swipes += 1 } } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 13DF01681F9D9CBD0092A320 /* ReorderableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */; }; 13DF016A1F9D9F600092A320 /* ReorderableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01691F9D9F600092A320 /* ReorderableViewController.swift */; }; 26271C8E1DAE9D3F0073E116 /* SingleSectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26271C8D1DAE9D3F0073E116 /* SingleSectionViewController.swift */; }; 26271C921DAE9EFC0073E116 /* NibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26271C911DAE9EFC0073E116 /* NibCell.xib */; }; 26271C941DAE9F050073E116 /* NibCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26271C931DAE9F050073E116 /* NibCell.swift */; }; 292658571E749EDC0041B56D /* CalendarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658561E749EDC0041B56D /* CalendarViewController.swift */; }; 292658591E749F820041B56D /* Month.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658581E749F820041B56D /* Month.swift */; }; 292658601E74A19F0041B56D /* DayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2926585F1E74A19F0041B56D /* DayViewModel.swift */; }; 292658641E74A2550041B56D /* MonthSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658631E74A2550041B56D /* MonthSectionController.swift */; }; 292658661E74A49D0041B56D /* CalendarDayCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658651E74A49D0041B56D /* CalendarDayCell.swift */; }; 292658681E74AE8A0041B56D /* MonthTitleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658671E74AE8A0041B56D /* MonthTitleViewModel.swift */; }; 2926586A1E74AEFE0041B56D /* MonthTitleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292658691E74AEFE0041B56D /* MonthTitleCell.swift */; }; 2942FF8C1D9F39E00015D24B /* DemoSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF831D9F39E00015D24B /* DemoSectionController.swift */; }; 2942FF8D1D9F39E00015D24B /* EmbeddedSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF841D9F39E00015D24B /* EmbeddedSectionController.swift */; }; 2942FF8E1D9F39E00015D24B /* ExpandableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF851D9F39E00015D24B /* ExpandableSectionController.swift */; }; 2942FF8F1D9F39E00015D24B /* GridSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF861D9F39E00015D24B /* GridSectionController.swift */; }; 2942FF901D9F39E00015D24B /* HorizontalSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF871D9F39E00015D24B /* HorizontalSectionController.swift */; }; 2942FF911D9F39E00015D24B /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF881D9F39E00015D24B /* LabelSectionController.swift */; }; 2942FF921D9F39E00015D24B /* RemoveSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF891D9F39E00015D24B /* RemoveSectionController.swift */; }; 2942FF931D9F39E00015D24B /* SearchSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF8A1D9F39E00015D24B /* SearchSectionController.swift */; }; 2942FF941D9F39E00015D24B /* UserSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF8B1D9F39E00015D24B /* UserSectionController.swift */; }; 29459C001DBE48E200F05375 /* DiffTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29459BFF1DBE48E200F05375 /* DiffTableViewController.swift */; }; 295D8A9A1E92EC96001F7C06 /* PostSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 295D8A991E92EC96001F7C06 /* PostSectionController.m */; }; 295D8A9D1E92ECDE001F7C06 /* Post.m in Sources */ = {isa = PBXBuildFile; fileRef = 295D8A9C1E92ECDE001F7C06 /* Post.m */; }; 2961B38E1D68B031001C9451 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B38D1D68B031001C9451 /* AppDelegate.swift */; }; 2961B3951D68B031001C9451 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2961B3941D68B031001C9451 /* Assets.xcassets */; }; 2961B3981D68B031001C9451 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2961B3961D68B031001C9451 /* LaunchScreen.storyboard */; }; 2961B3AB1D68B0B5001C9451 /* DemosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A41D68B0B5001C9451 /* DemosViewController.swift */; }; 2961B3AC1D68B0B5001C9451 /* LoadMoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A51D68B0B5001C9451 /* LoadMoreViewController.swift */; }; 2961B3AD1D68B0B5001C9451 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A71D68B0B5001C9451 /* LabelCell.swift */; }; 2961B3AE1D68B0B5001C9451 /* SpinnerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A81D68B0B5001C9451 /* SpinnerCell.swift */; }; 2961B3B01D68B28E001C9451 /* SearchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3AF1D68B28E001C9451 /* SearchCell.swift */; }; 29628F141D91905A0026B15A /* DetailLabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29628F131D91905A0026B15A /* DetailLabelCell.swift */; }; 296DD7531DD2147500206780 /* SelfSizingCellsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296DD7521DD2147500206780 /* SelfSizingCellsViewController.swift */; }; 296DD7551DD2150600206780 /* SelfSizingSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296DD7541DD2150600206780 /* SelfSizingSectionController.swift */; }; 296DD7571DD2163800206780 /* ManuallySelfSizingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296DD7561DD2163800206780 /* ManuallySelfSizingCell.swift */; }; 296DD7591DD2174200206780 /* NibSelfSizingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296DD7581DD2174200206780 /* NibSelfSizingCell.swift */; }; 296DD75B1DD217C000206780 /* NibSelfSizingCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 296DD75A1DD217C000206780 /* NibSelfSizingCell.xib */; }; 296DD75D1DD21ADA00206780 /* SelectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296DD75C1DD21ADA00206780 /* SelectionModel.swift */; }; 297546FA1DD25384002A6F89 /* FullWidthSelfSizingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297546F91DD25384002A6F89 /* FullWidthSelfSizingCell.swift */; }; 2981BA351DB868A500A987F9 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2981BA341DB868A500A987F9 /* ImageCell.swift */; }; 2981BA371DB869FF00A987F9 /* WorkingRangeSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2981BA361DB869FF00A987F9 /* WorkingRangeSectionController.swift */; }; 2981BA391DB874BB00A987F9 /* WorkingRangeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2981BA381DB874BB00A987F9 /* WorkingRangeViewController.swift */; }; 299068281D75BFEC00A62888 /* MixedDataViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 299068271D75BFEC00A62888 /* MixedDataViewController.swift */; }; 2991F9191D7BADC900B0C58F /* CenterLabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F9181D7BADC900B0C58F /* CenterLabelCell.swift */; }; 2991F91E1D7BB30C00B0C58F /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F91D1D7BB30C00B0C58F /* User.swift */; }; 2991F9241D7BB89F00B0C58F /* NestedAdapterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F9231D7BB89F00B0C58F /* NestedAdapterViewController.swift */; }; 2991F9281D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F9271D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift */; }; 2991F92C1D7BBE5400B0C58F /* RemoveCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F92B1D7BBE5400B0C58F /* RemoveCell.swift */; }; 2991F9301D7BC0E400B0C58F /* EmptyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2991F92F1D7BC0E400B0C58F /* EmptyViewController.swift */; }; 299B54001D6BD6630074A202 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 299B53FF1D6BD6630074A202 /* SearchViewController.swift */; }; 29C6297B1DCFD857004A5BB1 /* SupplementaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C6297A1DCFD857004A5BB1 /* SupplementaryViewController.swift */; }; 29C6297D1DCFD8E5004A5BB1 /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C6297C1DCFD8E5004A5BB1 /* FeedItem.swift */; }; 29C6297F1DCFD9E9004A5BB1 /* FeedItemSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C6297E1DCFD9E9004A5BB1 /* FeedItemSectionController.swift */; }; 29C629811DCFDAF3004A5BB1 /* UserHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 29C629801DCFDAF3004A5BB1 /* UserHeaderView.xib */; }; 29C629831DCFDB57004A5BB1 /* UserHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29C629821DCFDB57004A5BB1 /* UserHeaderView.swift */; }; 29D2E4AD1DD69B6000CD255D /* DisplaySectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D2E4AC1DD69B6000CD255D /* DisplaySectionController.swift */; }; 29D2E4AF1DD69C0E00CD255D /* DisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D2E4AE1DD69C0E00CD255D /* DisplayViewController.swift */; }; 29F7E2AA1E9283FF00197586 /* AnnouncingDepsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7E2A91E9283FF00197586 /* AnnouncingDepsViewController.swift */; }; 29F7E2AD1E92843A00197586 /* IncrementAnnouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7E2AC1E92843A00197586 /* IncrementAnnouncer.swift */; }; 29F7E2AF1E92858500197586 /* ListeningSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29F7E2AE1E92858500197586 /* ListeningSectionController.swift */; }; 4119B2AB2D89BB1E00D397BC /* ActivityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4119B2AA2D89BB1200D397BC /* ActivityItem.swift */; }; 4119B2AD2D89BBF000D397BC /* ActivityComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4119B2AC2D89BBE200D397BC /* ActivityComposableSectionController.swift */; }; 41A162952D8A0C82002A5A8F /* SelectionComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A162942D8A0C7A002A5A8F /* SelectionComposableSectionController.swift */; }; 56C05B691E49B2120026DB39 /* ObjcDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C05B681E49B2120026DB39 /* ObjcDemoViewController.m */; }; 56C05B6C1E49B2E80026DB39 /* UserInfoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C05B6B1E49B2E80026DB39 /* UserInfoCell.m */; }; 56C05B721E49B32A0026DB39 /* CommentCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C05B711E49B32A0026DB39 /* CommentCell.m */; }; 56C05B751E49B33C0026DB39 /* InteractiveCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C05B741E49B33C0026DB39 /* InteractiveCell.m */; }; 56C05B781E49B3A50026DB39 /* PhotoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 56C05B771E49B3A50026DB39 /* PhotoCell.m */; }; 576D20072B2CB50E0012C5B8 /* CompositionLayoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20062B2CB50E0012C5B8 /* CompositionLayoutViewController.swift */; }; 576D20092B2CB6C20012C5B8 /* SwipeActionComposabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20082B2CB6C20012C5B8 /* SwipeActionComposabelSectionController.swift */; }; 576D200B2B2CC6DF0012C5B8 /* CompositionLayoutCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D200A2B2CC6DF0012C5B8 /* CompositionLayoutCell.swift */; }; 576D200E2B2CE9EC0012C5B8 /* ExpandableComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D200D2B2CE9EC0012C5B8 /* ExpandableComposableSectionController.swift */; }; 576D20102B2CEC4E0012C5B8 /* GridComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D200F2B2CEC4E0012C5B8 /* GridComposableSectionController.swift */; }; 576D20122B2CF04F0012C5B8 /* UserComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20112B2CF04F0012C5B8 /* UserComposableSectionController.swift */; }; 576D20142B2CF15C0012C5B8 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20132B2CF15C0012C5B8 /* GridItem.swift */; }; 576D20162B2CF1A50012C5B8 /* HorizontalCardsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20152B2CF1A50012C5B8 /* HorizontalCardsSection.swift */; }; 576D20182B2CF24F0012C5B8 /* HorizontalComposableSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20172B2CF24F0012C5B8 /* HorizontalComposableSectionController.swift */; }; 576D201A2B2CF82C0012C5B8 /* SwipeActionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576D20192B2CF82C0012C5B8 /* SwipeActionSection.swift */; }; 7D4506D22DC001670029D095 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506D12DC001670029D095 /* FeedViewController.swift */; }; 7D4506D52DC001C40029D095 /* PostModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506D42DC001C40029D095 /* PostModel.swift */; }; 7D4506D62DC001C40029D095 /* LoadingCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506D32DC001C40029D095 /* LoadingCellModel.swift */; }; 7D4506D82DC001DD0029D095 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506D72DC001DD0029D095 /* APIService.swift */; }; 7D4506DB2DC001FC0029D095 /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506DA2DC001FC0029D095 /* PostCell.swift */; }; 7D4506DC2DC001FC0029D095 /* LoadingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506D92DC001FC0029D095 /* LoadingCell.swift */; }; 7D4506E02DC0020C0029D095 /* LoadingSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506DD2DC0020C0029D095 /* LoadingSectionController.swift */; }; 7D4506E22DC0020C0029D095 /* PostSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506DE2DC0020C0029D095 /* PostSectionController.swift */; }; 7D4506E62DC0052C0029D095 /* FeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4506E52DC0052C0029D095 /* FeedViewController.swift */; }; 7D9DC0F82DC379A000BFB68C /* PostSectionControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D9DC0F62DC379A000BFB68C /* PostSectionControllerDelegate.swift */; }; 821BC4B61DB8B3DC00172ED0 /* StoryboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4B51DB8B3DC00172ED0 /* StoryboardViewController.swift */; }; 821BC4B81DB8B48300172ED0 /* StoryboardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4B71DB8B48300172ED0 /* StoryboardCell.swift */; }; 821BC4BA1DB8B61200172ED0 /* StoryboardLabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4B91DB8B61200172ED0 /* StoryboardLabelSectionController.swift */; }; 82D91B691DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82D91B681DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift */; }; 9518E3C31DED03520008CC46 /* UITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9518E3C21DED03510008CC46 /* UITestCase.swift */; }; 9518E3C51DED057E0008CC46 /* LoadMoreViewControllerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9518E3C41DED057E0008CC46 /* LoadMoreViewControllerUITests.swift */; }; 9518E3C71DED18370008CC46 /* SearchViewControllerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9518E3C61DED18370008CC46 /* SearchViewControllerUITests.swift */; }; 9518E3C91DED1BCB0008CC46 /* MixedDataViewControllerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9518E3C81DED1BCB0008CC46 /* MixedDataViewControllerUITests.swift */; }; 9518E3CB1DED27840008CC46 /* DemosViewControllerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9518E3CA1DED27840008CC46 /* DemosViewControllerUITests.swift */; }; 986FB7131DBBA60900A65C18 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 986FB7121DBBA60900A65C18 /* Messages.framework */; }; 986FB7161DBBA60900A65C18 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986FB7151DBBA60900A65C18 /* MessagesViewController.swift */; }; 986FB71B1DBBA60900A65C18 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 986FB71A1DBBA60900A65C18 /* Assets.xcassets */; }; 986FB71F1DBBA60900A65C18 /* IGListKitMessageExample.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 986FB7111DBBA60900A65C18 /* IGListKitMessageExample.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 986FB7281DBBA9B200A65C18 /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF881D9F39E00015D24B /* LabelSectionController.swift */; }; 986FB7291DBBA9C900A65C18 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A71D68B0B5001C9451 /* LabelCell.swift */; }; 986FB7311DBBAD8600A65C18 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 986FB7301DBBAD8600A65C18 /* NotificationCenter.framework */; }; 986FB7341DBBAD8600A65C18 /* TodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986FB7331DBBAD8600A65C18 /* TodayViewController.swift */; }; 986FB73B1DBBAD8600A65C18 /* IGListKitTodayExample.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 986FB72F1DBBAD8600A65C18 /* IGListKitTodayExample.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 986FB7401DBBAEA600A65C18 /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942FF881D9F39E00015D24B /* LabelSectionController.swift */; }; 986FB7411DBBAEA900A65C18 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2961B3A71D68B0B5001C9451 /* LabelCell.swift */; }; 98B4DBF41DC2937A002BA58A /* Demo.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98B4DBF31DC2937A002BA58A /* Demo.storyboard */; }; 9DB684E1251B10C2002023DD /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DB684E0251B10C2002023DD /* UIColor+Extension.swift */; }; 9DFF4CFB251F089E0048474A /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DB684E0251B10C2002023DD /* UIColor+Extension.swift */; }; 9DFF4D00251F089F0048474A /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DB684E0251B10C2002023DD /* UIColor+Extension.swift */; }; 9DFF4D06251F08F50048474A /* UIActivityIndicatorView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DFF4D05251F08F50048474A /* UIActivityIndicatorView+Extension.swift */; }; BAB336E42760ECC1007B4098 /* IGListDiffKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336E32760ECC1007B4098 /* IGListDiffKit */; }; BAB336E62760ECC1007B4098 /* IGListKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336E52760ECC1007B4098 /* IGListKit */; }; BAB336E82760ECC1007B4098 /* IGListSwiftKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336E72760ECC1007B4098 /* IGListSwiftKit */; }; BAB336EA2760ECE5007B4098 /* IGListKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336E92760ECE5007B4098 /* IGListKit */; }; BAB336EC2760ECFE007B4098 /* IGListKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336EB2760ECFE007B4098 /* IGListKit */; }; BAB336EE2760ED0C007B4098 /* IGListSwiftKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336ED2760ED0C007B4098 /* IGListSwiftKit */; }; BAB336F02760ED28007B4098 /* IGListDiffKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336EF2760ED28007B4098 /* IGListDiffKit */; }; BAB336F22760ED55007B4098 /* IGListDiffKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336F12760ED55007B4098 /* IGListDiffKit */; }; BAB336F42760EDCB007B4098 /* IGListSwiftKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336F32760EDCB007B4098 /* IGListSwiftKit */; }; EB0289A2202A11E0007E17D5 /* PersonModel.m in Sources */ = {isa = PBXBuildFile; fileRef = EB02899F202A11DF007E17D5 /* PersonModel.m */; }; EB0289A3202A11E0007E17D5 /* PersonModel.value in Resources */ = {isa = PBXBuildFile; fileRef = EB0289A0202A11DF007E17D5 /* PersonModel.value */; }; EB0289A6202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = EB0289A5202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.m */; }; EB0289A9202A1507007E17D5 /* PersonSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = EB0289A8202A1507007E17D5 /* PersonSectionController.m */; }; EB0289AC202A1705007E17D5 /* PersonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = EB0289AA202A1705007E17D5 /* PersonCell.m */; }; FF73FF9C1FBA4B4400F2AB06 /* UserFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF73FF9B1FBA4B4400F2AB06 /* UserFooterView.swift */; }; FF73FF9E1FBA4B5500F2AB06 /* UserFooterView.xib in Resources */ = {isa = PBXBuildFile; fileRef = FF73FF9D1FBA4B5500F2AB06 /* UserFooterView.xib */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 95F7F9161DE5006C00A64FEE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2961B3821D68B031001C9451 /* Project object */; proxyType = 1; remoteGlobalIDString = 2961B3891D68B031001C9451; remoteInfo = IGListKitExamples; }; 986FB71D1DBBA60900A65C18 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2961B3821D68B031001C9451 /* Project object */; proxyType = 1; remoteGlobalIDString = 986FB7101DBBA60900A65C18; remoteInfo = MessageExtension; }; 986FB7391DBBAD8600A65C18 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2961B3821D68B031001C9451 /* Project object */; proxyType = 1; remoteGlobalIDString = 986FB72E1DBBAD8600A65C18; remoteInfo = IGListKitTodayExample; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 986FB7231DBBA60900A65C18 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( 986FB73B1DBBAD8600A65C18 /* IGListKitTodayExample.appex in Embed Foundation Extensions */, 986FB71F1DBBA60900A65C18 /* IGListKitMessageExample.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableSectionController.swift; sourceTree = ""; }; 13DF01691F9D9F600092A320 /* ReorderableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableViewController.swift; sourceTree = ""; }; 26271C8D1DAE9D3F0073E116 /* SingleSectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSectionViewController.swift; sourceTree = ""; }; 26271C911DAE9EFC0073E116 /* NibCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NibCell.xib; sourceTree = ""; }; 26271C931DAE9F050073E116 /* NibCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibCell.swift; sourceTree = ""; }; 292658561E749EDC0041B56D /* CalendarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarViewController.swift; sourceTree = ""; }; 292658581E749F820041B56D /* Month.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Month.swift; sourceTree = ""; }; 2926585F1E74A19F0041B56D /* DayViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DayViewModel.swift; sourceTree = ""; }; 292658631E74A2550041B56D /* MonthSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonthSectionController.swift; sourceTree = ""; }; 292658651E74A49D0041B56D /* CalendarDayCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarDayCell.swift; sourceTree = ""; }; 292658671E74AE8A0041B56D /* MonthTitleViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonthTitleViewModel.swift; sourceTree = ""; }; 292658691E74AEFE0041B56D /* MonthTitleCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonthTitleCell.swift; sourceTree = ""; }; 2942FF831D9F39E00015D24B /* DemoSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemoSectionController.swift; sourceTree = ""; }; 2942FF841D9F39E00015D24B /* EmbeddedSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedSectionController.swift; sourceTree = ""; }; 2942FF851D9F39E00015D24B /* ExpandableSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpandableSectionController.swift; sourceTree = ""; }; 2942FF861D9F39E00015D24B /* GridSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridSectionController.swift; sourceTree = ""; }; 2942FF871D9F39E00015D24B /* HorizontalSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalSectionController.swift; sourceTree = ""; }; 2942FF881D9F39E00015D24B /* LabelSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelSectionController.swift; sourceTree = ""; }; 2942FF891D9F39E00015D24B /* RemoveSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveSectionController.swift; sourceTree = ""; }; 2942FF8A1D9F39E00015D24B /* SearchSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchSectionController.swift; sourceTree = ""; }; 2942FF8B1D9F39E00015D24B /* UserSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSectionController.swift; sourceTree = ""; }; 29459BFF1DBE48E200F05375 /* DiffTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffTableViewController.swift; sourceTree = ""; }; 295D8A981E92EC96001F7C06 /* PostSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostSectionController.h; sourceTree = ""; }; 295D8A991E92EC96001F7C06 /* PostSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostSectionController.m; sourceTree = ""; }; 295D8A9B1E92ECDE001F7C06 /* Post.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Post.h; sourceTree = ""; }; 295D8A9C1E92ECDE001F7C06 /* Post.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Post.m; sourceTree = ""; }; 2961B38A1D68B031001C9451 /* IGListKitExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IGListKitExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2961B38D1D68B031001C9451 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 2961B3941D68B031001C9451 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2961B3971D68B031001C9451 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 2961B3991D68B031001C9451 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 2961B3A41D68B0B5001C9451 /* DemosViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemosViewController.swift; sourceTree = ""; }; 2961B3A51D68B0B5001C9451 /* LoadMoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreViewController.swift; sourceTree = ""; }; 2961B3A71D68B0B5001C9451 /* LabelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelCell.swift; sourceTree = ""; }; 2961B3A81D68B0B5001C9451 /* SpinnerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpinnerCell.swift; sourceTree = ""; }; 2961B3AF1D68B28E001C9451 /* SearchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchCell.swift; sourceTree = ""; }; 29628F131D91905A0026B15A /* DetailLabelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailLabelCell.swift; sourceTree = ""; }; 296DD7521DD2147500206780 /* SelfSizingCellsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingCellsViewController.swift; sourceTree = ""; }; 296DD7541DD2150600206780 /* SelfSizingSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelfSizingSectionController.swift; sourceTree = ""; }; 296DD7561DD2163800206780 /* ManuallySelfSizingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManuallySelfSizingCell.swift; sourceTree = ""; }; 296DD7581DD2174200206780 /* NibSelfSizingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibSelfSizingCell.swift; sourceTree = ""; }; 296DD75A1DD217C000206780 /* NibSelfSizingCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NibSelfSizingCell.xib; sourceTree = ""; }; 296DD75C1DD21ADA00206780 /* SelectionModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionModel.swift; sourceTree = ""; }; 297546F91DD25384002A6F89 /* FullWidthSelfSizingCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullWidthSelfSizingCell.swift; sourceTree = ""; }; 2981BA341DB868A500A987F9 /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; 2981BA361DB869FF00A987F9 /* WorkingRangeSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkingRangeSectionController.swift; sourceTree = ""; }; 2981BA381DB874BB00A987F9 /* WorkingRangeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkingRangeViewController.swift; sourceTree = ""; }; 299068271D75BFEC00A62888 /* MixedDataViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixedDataViewController.swift; sourceTree = ""; }; 2991F9181D7BADC900B0C58F /* CenterLabelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenterLabelCell.swift; sourceTree = ""; }; 2991F91D1D7BB30C00B0C58F /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 2991F9231D7BB89F00B0C58F /* NestedAdapterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NestedAdapterViewController.swift; sourceTree = ""; }; 2991F9271D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedCollectionViewCell.swift; sourceTree = ""; }; 2991F92B1D7BBE5400B0C58F /* RemoveCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoveCell.swift; sourceTree = ""; }; 2991F92F1D7BC0E400B0C58F /* EmptyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyViewController.swift; sourceTree = ""; }; 299B53FF1D6BD6630074A202 /* SearchViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 29C6297A1DCFD857004A5BB1 /* SupplementaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupplementaryViewController.swift; sourceTree = ""; }; 29C6297C1DCFD8E5004A5BB1 /* FeedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedItem.swift; sourceTree = ""; }; 29C6297E1DCFD9E9004A5BB1 /* FeedItemSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedItemSectionController.swift; sourceTree = ""; }; 29C629801DCFDAF3004A5BB1 /* UserHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UserHeaderView.xib; sourceTree = ""; }; 29C629821DCFDB57004A5BB1 /* UserHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserHeaderView.swift; sourceTree = ""; }; 29D2E4AC1DD69B6000CD255D /* DisplaySectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplaySectionController.swift; sourceTree = ""; }; 29D2E4AE1DD69C0E00CD255D /* DisplayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayViewController.swift; sourceTree = ""; }; 29F7E2A91E9283FF00197586 /* AnnouncingDepsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnouncingDepsViewController.swift; sourceTree = ""; }; 29F7E2AC1E92843A00197586 /* IncrementAnnouncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncrementAnnouncer.swift; sourceTree = ""; }; 29F7E2AE1E92858500197586 /* ListeningSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListeningSectionController.swift; sourceTree = ""; }; 4119B2AA2D89BB1200D397BC /* ActivityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItem.swift; sourceTree = ""; }; 4119B2AC2D89BBE200D397BC /* ActivityComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityComposableSectionController.swift; sourceTree = ""; }; 41A162942D8A0C7A002A5A8F /* SelectionComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionComposableSectionController.swift; sourceTree = ""; }; 49DEF56A3C9C414B461D113F /* Pods_IGListKitTodayExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitTodayExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56C05B661E49B2110026DB39 /* IGListKitExamples-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IGListKitExamples-Bridging-Header.h"; sourceTree = ""; }; 56C05B671E49B2120026DB39 /* ObjcDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcDemoViewController.h; sourceTree = ""; }; 56C05B681E49B2120026DB39 /* ObjcDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcDemoViewController.m; sourceTree = ""; }; 56C05B6A1E49B2E80026DB39 /* UserInfoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserInfoCell.h; sourceTree = ""; }; 56C05B6B1E49B2E80026DB39 /* UserInfoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UserInfoCell.m; sourceTree = ""; }; 56C05B701E49B32A0026DB39 /* CommentCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CommentCell.h; sourceTree = ""; }; 56C05B711E49B32A0026DB39 /* CommentCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CommentCell.m; sourceTree = ""; }; 56C05B731E49B33C0026DB39 /* InteractiveCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InteractiveCell.h; sourceTree = ""; }; 56C05B741E49B33C0026DB39 /* InteractiveCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteractiveCell.m; sourceTree = ""; }; 56C05B761E49B3A50026DB39 /* PhotoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotoCell.h; sourceTree = ""; }; 56C05B771E49B3A50026DB39 /* PhotoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotoCell.m; sourceTree = ""; }; 576D20062B2CB50E0012C5B8 /* CompositionLayoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionLayoutViewController.swift; sourceTree = ""; }; 576D20082B2CB6C20012C5B8 /* SwipeActionComposabelSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionComposabelSectionController.swift; sourceTree = ""; }; 576D200A2B2CC6DF0012C5B8 /* CompositionLayoutCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionLayoutCell.swift; sourceTree = ""; }; 576D200D2B2CE9EC0012C5B8 /* ExpandableComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableComposableSectionController.swift; sourceTree = ""; }; 576D200F2B2CEC4E0012C5B8 /* GridComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridComposableSectionController.swift; sourceTree = ""; }; 576D20112B2CF04F0012C5B8 /* UserComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserComposableSectionController.swift; sourceTree = ""; }; 576D20132B2CF15C0012C5B8 /* GridItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; 576D20152B2CF1A50012C5B8 /* HorizontalCardsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCardsSection.swift; sourceTree = ""; }; 576D20172B2CF24F0012C5B8 /* HorizontalComposableSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalComposableSectionController.swift; sourceTree = ""; }; 576D20192B2CF82C0012C5B8 /* SwipeActionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeActionSection.swift; sourceTree = ""; }; 7D4506D12DC001670029D095 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 7D4506D32DC001C40029D095 /* LoadingCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCellModel.swift; sourceTree = ""; }; 7D4506D42DC001C40029D095 /* PostModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostModel.swift; sourceTree = ""; }; 7D4506D72DC001DD0029D095 /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; 7D4506D92DC001FC0029D095 /* LoadingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCell.swift; sourceTree = ""; }; 7D4506DA2DC001FC0029D095 /* PostCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCell.swift; sourceTree = ""; }; 7D4506DD2DC0020C0029D095 /* LoadingSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSectionController.swift; sourceTree = ""; }; 7D4506DE2DC0020C0029D095 /* PostSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSectionController.swift; sourceTree = ""; }; 7D4506E52DC0052C0029D095 /* FeedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedViewController.swift; sourceTree = ""; }; 7D9DC0F62DC379A000BFB68C /* PostSectionControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSectionControllerDelegate.swift; sourceTree = ""; }; 821BC4B51DB8B3DC00172ED0 /* StoryboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardViewController.swift; sourceTree = ""; }; 821BC4B71DB8B48300172ED0 /* StoryboardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardCell.swift; sourceTree = ""; }; 821BC4B91DB8B61200172ED0 /* StoryboardLabelSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoryboardLabelSectionController.swift; sourceTree = ""; }; 82D91B681DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSectionStoryboardViewController.swift; sourceTree = ""; }; 9518E3C21DED03510008CC46 /* UITestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITestCase.swift; sourceTree = ""; }; 9518E3C41DED057E0008CC46 /* LoadMoreViewControllerUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreViewControllerUITests.swift; sourceTree = ""; }; 9518E3C61DED18370008CC46 /* SearchViewControllerUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchViewControllerUITests.swift; sourceTree = ""; }; 9518E3C81DED1BCB0008CC46 /* MixedDataViewControllerUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MixedDataViewControllerUITests.swift; sourceTree = ""; }; 9518E3CA1DED27840008CC46 /* DemosViewControllerUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DemosViewControllerUITests.swift; sourceTree = ""; }; 95F7F9111DE5006B00A64FEE /* IGListKitExamples-UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "IGListKitExamples-UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 95F7F9151DE5006C00A64FEE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 986FB7111DBBA60900A65C18 /* IGListKitMessageExample.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IGListKitMessageExample.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 986FB7121DBBA60900A65C18 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; 986FB7151DBBA60900A65C18 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; 986FB71A1DBBA60900A65C18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 986FB71C1DBBA60900A65C18 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 986FB72F1DBBAD8600A65C18 /* IGListKitTodayExample.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IGListKitTodayExample.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 986FB7301DBBAD8600A65C18 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; 986FB7331DBBAD8600A65C18 /* TodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewController.swift; sourceTree = ""; }; 986FB7381DBBAD8600A65C18 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 98B4DBF31DC2937A002BA58A /* Demo.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Demo.storyboard; path = IGListKitExamples/Storyboard/Demo.storyboard; sourceTree = ""; }; 9DB684E0251B10C2002023DD /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; 9DFF4D05251F08F50048474A /* UIActivityIndicatorView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivityIndicatorView+Extension.swift"; sourceTree = ""; }; B7B1FA2EFFA5E994FEE587DE /* Pods_IGListKitMessageExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitMessageExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BAB336E22760EC95007B4098 /* IGListKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = IGListKit; path = ../..; sourceTree = ""; }; BAB337022760F50D007B4098 /* IGListKitExamples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IGListKitExamples.entitlements; sourceTree = ""; }; EB02899F202A11DF007E17D5 /* PersonModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PersonModel.m; sourceTree = ""; }; EB0289A0202A11DF007E17D5 /* PersonModel.value */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = PersonModel.value; sourceTree = ""; }; EB0289A1202A11DF007E17D5 /* PersonModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PersonModel.h; sourceTree = ""; }; EB0289A4202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjcGeneratedModelDemoViewController.h; sourceTree = ""; }; EB0289A5202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjcGeneratedModelDemoViewController.m; sourceTree = ""; }; EB0289A7202A1507007E17D5 /* PersonSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PersonSectionController.h; sourceTree = ""; }; EB0289A8202A1507007E17D5 /* PersonSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PersonSectionController.m; sourceTree = ""; }; EB0289AA202A1705007E17D5 /* PersonCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PersonCell.m; sourceTree = ""; }; EB0289AB202A1705007E17D5 /* PersonCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PersonCell.h; sourceTree = ""; }; FE7316EF8201F89655D39E53 /* Pods_IGListKitExamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitExamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FF73FF9B1FBA4B4400F2AB06 /* UserFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFooterView.swift; sourceTree = ""; }; FF73FF9D1FBA4B5500F2AB06 /* UserFooterView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UserFooterView.xib; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 2961B3871D68B031001C9451 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BAB336E42760ECC1007B4098 /* IGListDiffKit in Frameworks */, BAB336E62760ECC1007B4098 /* IGListKit in Frameworks */, BAB336E82760ECC1007B4098 /* IGListSwiftKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 95F7F90E1DE5006B00A64FEE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 986FB70E1DBBA60900A65C18 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 986FB7131DBBA60900A65C18 /* Messages.framework in Frameworks */, BAB336F22760ED55007B4098 /* IGListDiffKit in Frameworks */, BAB336EC2760ECFE007B4098 /* IGListKit in Frameworks */, BAB336F42760EDCB007B4098 /* IGListSwiftKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 986FB72C1DBBAD8600A65C18 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 986FB7311DBBAD8600A65C18 /* NotificationCenter.framework in Frameworks */, BAB336EA2760ECE5007B4098 /* IGListKit in Frameworks */, BAB336F02760ED28007B4098 /* IGListDiffKit in Frameworks */, BAB336EE2760ED0C007B4098 /* IGListSwiftKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 0853E82EF5C5E4BD4E0760E3 /* Frameworks */ = { isa = PBXGroup; children = ( 986FB7121DBBA60900A65C18 /* Messages.framework */, 986FB7301DBBAD8600A65C18 /* NotificationCenter.framework */, FE7316EF8201F89655D39E53 /* Pods_IGListKitExamples.framework */, B7B1FA2EFFA5E994FEE587DE /* Pods_IGListKitMessageExample.framework */, 49DEF56A3C9C414B461D113F /* Pods_IGListKitTodayExample.framework */, ); name = Frameworks; sourceTree = ""; }; 2926585C1E74A0360041B56D /* ViewModels */ = { isa = PBXGroup; children = ( 2926585F1E74A19F0041B56D /* DayViewModel.swift */, 292658671E74AE8A0041B56D /* MonthTitleViewModel.swift */, ); path = ViewModels; sourceTree = ""; }; 2942FF821D9F39E00015D24B /* SectionControllers */ = { isa = PBXGroup; children = ( 2942FF831D9F39E00015D24B /* DemoSectionController.swift */, 29D2E4AC1DD69B6000CD255D /* DisplaySectionController.swift */, 2942FF841D9F39E00015D24B /* EmbeddedSectionController.swift */, 2942FF851D9F39E00015D24B /* ExpandableSectionController.swift */, 29C6297E1DCFD9E9004A5BB1 /* FeedItemSectionController.swift */, 2942FF861D9F39E00015D24B /* GridSectionController.swift */, 2942FF871D9F39E00015D24B /* HorizontalSectionController.swift */, 2942FF881D9F39E00015D24B /* LabelSectionController.swift */, 29F7E2AE1E92858500197586 /* ListeningSectionController.swift */, 292658631E74A2550041B56D /* MonthSectionController.swift */, EB0289A7202A1507007E17D5 /* PersonSectionController.h */, EB0289A8202A1507007E17D5 /* PersonSectionController.m */, 295D8A981E92EC96001F7C06 /* PostSectionController.h */, 295D8A991E92EC96001F7C06 /* PostSectionController.m */, 2942FF891D9F39E00015D24B /* RemoveSectionController.swift */, 13DF01671F9D9CBD0092A320 /* ReorderableSectionController.swift */, 2942FF8A1D9F39E00015D24B /* SearchSectionController.swift */, 296DD7541DD2150600206780 /* SelfSizingSectionController.swift */, 821BC4B91DB8B61200172ED0 /* StoryboardLabelSectionController.swift */, 2942FF8B1D9F39E00015D24B /* UserSectionController.swift */, 2981BA361DB869FF00A987F9 /* WorkingRangeSectionController.swift */, 576D200C2B2CE9C60012C5B8 /* With Composable Layout */, 7D4506DD2DC0020C0029D095 /* LoadingSectionController.swift */, 7D4506DE2DC0020C0029D095 /* PostSectionController.swift */, ); path = SectionControllers; sourceTree = ""; }; 2961B3811D68B031001C9451 = { isa = PBXGroup; children = ( 2961B38C1D68B031001C9451 /* IGListKitExamples */, 986FB7141DBBA60900A65C18 /* IGListKitMessageExample */, 986FB7321DBBAD8600A65C18 /* IGListKitTodayExample */, 95F7F9121DE5006C00A64FEE /* IGListKitExamples-UITests */, BAB336E12760EC95007B4098 /* Packages */, 2961B38B1D68B031001C9451 /* Products */, 0853E82EF5C5E4BD4E0760E3 /* Frameworks */, ); sourceTree = ""; }; 2961B38B1D68B031001C9451 /* Products */ = { isa = PBXGroup; children = ( 2961B38A1D68B031001C9451 /* IGListKitExamples.app */, 986FB7111DBBA60900A65C18 /* IGListKitMessageExample.appex */, 986FB72F1DBBAD8600A65C18 /* IGListKitTodayExample.appex */, 95F7F9111DE5006B00A64FEE /* IGListKitExamples-UITests.xctest */, ); name = Products; sourceTree = ""; }; 2961B38C1D68B031001C9451 /* IGListKitExamples */ = { isa = PBXGroup; children = ( 7D9DC0F72DC379A000BFB68C /* DelegateProtocols */, 2961B38D1D68B031001C9451 /* AppDelegate.swift */, 2961B3941D68B031001C9451 /* Assets.xcassets */, 9DB684DF251B1093002023DD /* Extensions */, BAB337022760F50D007B4098 /* IGListKitExamples.entitlements */, 2961B3991D68B031001C9451 /* Info.plist */, 2961B3961D68B031001C9451 /* LaunchScreen.storyboard */, 2991F91C1D7BB30300B0C58F /* Models */, 2942FF821D9F39E00015D24B /* SectionControllers */, 822B29F21DB8AA4700010000 /* Storyboard */, 29F7E2AB1E92842D00197586 /* Systems */, 2961B3A31D68B0B5001C9451 /* ViewControllers */, 2961B3A61D68B0B5001C9451 /* Views */, ); path = IGListKitExamples; sourceTree = ""; }; 2961B3A31D68B0B5001C9451 /* ViewControllers */ = { isa = PBXGroup; children = ( 7D4506D12DC001670029D095 /* FeedViewController.swift */, 29F7E2A91E9283FF00197586 /* AnnouncingDepsViewController.swift */, 292658561E749EDC0041B56D /* CalendarViewController.swift */, 2961B3A41D68B0B5001C9451 /* DemosViewController.swift */, 29459BFF1DBE48E200F05375 /* DiffTableViewController.swift */, 29D2E4AE1DD69C0E00CD255D /* DisplayViewController.swift */, 2991F92F1D7BC0E400B0C58F /* EmptyViewController.swift */, 56C05B661E49B2110026DB39 /* IGListKitExamples-Bridging-Header.h */, 2961B3A51D68B0B5001C9451 /* LoadMoreViewController.swift */, 299068271D75BFEC00A62888 /* MixedDataViewController.swift */, 2991F9231D7BB89F00B0C58F /* NestedAdapterViewController.swift */, 56C05B671E49B2120026DB39 /* ObjcDemoViewController.h */, 56C05B681E49B2120026DB39 /* ObjcDemoViewController.m */, EB0289A4202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.h */, EB0289A5202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.m */, 13DF01691F9D9F600092A320 /* ReorderableViewController.swift */, 299B53FF1D6BD6630074A202 /* SearchViewController.swift */, 296DD7521DD2147500206780 /* SelfSizingCellsViewController.swift */, 82D91B681DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift */, 26271C8D1DAE9D3F0073E116 /* SingleSectionViewController.swift */, 821BC4B51DB8B3DC00172ED0 /* StoryboardViewController.swift */, 29C6297A1DCFD857004A5BB1 /* SupplementaryViewController.swift */, 2981BA381DB874BB00A987F9 /* WorkingRangeViewController.swift */, 576D20062B2CB50E0012C5B8 /* CompositionLayoutViewController.swift */, ); path = ViewControllers; sourceTree = ""; }; 2961B3A61D68B0B5001C9451 /* Views */ = { isa = PBXGroup; children = ( 7D4506D92DC001FC0029D095 /* LoadingCell.swift */, 7D4506DA2DC001FC0029D095 /* PostCell.swift */, 292658651E74A49D0041B56D /* CalendarDayCell.swift */, 2991F9181D7BADC900B0C58F /* CenterLabelCell.swift */, 56C05B701E49B32A0026DB39 /* CommentCell.h */, 56C05B711E49B32A0026DB39 /* CommentCell.m */, 29628F131D91905A0026B15A /* DetailLabelCell.swift */, 2991F9271D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift */, 297546F91DD25384002A6F89 /* FullWidthSelfSizingCell.swift */, 2981BA341DB868A500A987F9 /* ImageCell.swift */, 56C05B731E49B33C0026DB39 /* InteractiveCell.h */, 56C05B741E49B33C0026DB39 /* InteractiveCell.m */, 2961B3A71D68B0B5001C9451 /* LabelCell.swift */, 296DD7561DD2163800206780 /* ManuallySelfSizingCell.swift */, 292658691E74AEFE0041B56D /* MonthTitleCell.swift */, 26271C931DAE9F050073E116 /* NibCell.swift */, 26271C911DAE9EFC0073E116 /* NibCell.xib */, 296DD7581DD2174200206780 /* NibSelfSizingCell.swift */, 296DD75A1DD217C000206780 /* NibSelfSizingCell.xib */, EB0289AB202A1705007E17D5 /* PersonCell.h */, EB0289AA202A1705007E17D5 /* PersonCell.m */, 56C05B761E49B3A50026DB39 /* PhotoCell.h */, 56C05B771E49B3A50026DB39 /* PhotoCell.m */, 2991F92B1D7BBE5400B0C58F /* RemoveCell.swift */, 2961B3AF1D68B28E001C9451 /* SearchCell.swift */, 2961B3A81D68B0B5001C9451 /* SpinnerCell.swift */, 821BC4B71DB8B48300172ED0 /* StoryboardCell.swift */, 29C629821DCFDB57004A5BB1 /* UserHeaderView.swift */, FF73FF9B1FBA4B4400F2AB06 /* UserFooterView.swift */, 576D200A2B2CC6DF0012C5B8 /* CompositionLayoutCell.swift */, FF73FF9D1FBA4B5500F2AB06 /* UserFooterView.xib */, 29C629801DCFDAF3004A5BB1 /* UserHeaderView.xib */, 56C05B6A1E49B2E80026DB39 /* UserInfoCell.h */, 56C05B6B1E49B2E80026DB39 /* UserInfoCell.m */, ); path = Views; sourceTree = ""; }; 2991F91C1D7BB30300B0C58F /* Models */ = { isa = PBXGroup; children = ( 7D4506D72DC001DD0029D095 /* APIService.swift */, 7D4506D32DC001C40029D095 /* LoadingCellModel.swift */, 7D4506D42DC001C40029D095 /* PostModel.swift */, 4119B2AA2D89BB1200D397BC /* ActivityItem.swift */, 29C6297C1DCFD8E5004A5BB1 /* FeedItem.swift */, 292658581E749F820041B56D /* Month.swift */, 295D8A9B1E92ECDE001F7C06 /* Post.h */, 295D8A9C1E92ECDE001F7C06 /* Post.m */, 296DD75C1DD21ADA00206780 /* SelectionModel.swift */, 2991F91D1D7BB30C00B0C58F /* User.swift */, 576D20132B2CF15C0012C5B8 /* GridItem.swift */, 576D20152B2CF1A50012C5B8 /* HorizontalCardsSection.swift */, 576D20192B2CF82C0012C5B8 /* SwipeActionSection.swift */, EB02899E202A11DF007E17D5 /* RemodelGeneratedModels */, 2926585C1E74A0360041B56D /* ViewModels */, ); path = Models; sourceTree = ""; }; 29F7E2AB1E92842D00197586 /* Systems */ = { isa = PBXGroup; children = ( 29F7E2AC1E92843A00197586 /* IncrementAnnouncer.swift */, ); path = Systems; sourceTree = ""; }; 576D200C2B2CE9C60012C5B8 /* With Composable Layout */ = { isa = PBXGroup; children = ( 41A162942D8A0C7A002A5A8F /* SelectionComposableSectionController.swift */, 4119B2AC2D89BBE200D397BC /* ActivityComposableSectionController.swift */, 576D200D2B2CE9EC0012C5B8 /* ExpandableComposableSectionController.swift */, 576D200F2B2CEC4E0012C5B8 /* GridComposableSectionController.swift */, 576D20112B2CF04F0012C5B8 /* UserComposableSectionController.swift */, 576D20172B2CF24F0012C5B8 /* HorizontalComposableSectionController.swift */, 576D20082B2CB6C20012C5B8 /* SwipeActionComposabelSectionController.swift */, ); path = "With Composable Layout"; sourceTree = ""; }; 7D9DC0F72DC379A000BFB68C /* DelegateProtocols */ = { isa = PBXGroup; children = ( 7D9DC0F62DC379A000BFB68C /* PostSectionControllerDelegate.swift */, ); path = DelegateProtocols; sourceTree = ""; }; 822B29F21DB8AA4700010000 /* Storyboard */ = { isa = PBXGroup; children = ( 98B4DBF31DC2937A002BA58A /* Demo.storyboard */, ); name = Storyboard; path = ..; sourceTree = ""; }; 95F7F9121DE5006C00A64FEE /* IGListKitExamples-UITests */ = { isa = PBXGroup; children = ( 7D4506E52DC0052C0029D095 /* FeedViewController.swift */, 9518E3CA1DED27840008CC46 /* DemosViewControllerUITests.swift */, 95F7F9151DE5006C00A64FEE /* Info.plist */, 9518E3C41DED057E0008CC46 /* LoadMoreViewControllerUITests.swift */, 9518E3C81DED1BCB0008CC46 /* MixedDataViewControllerUITests.swift */, 9518E3C61DED18370008CC46 /* SearchViewControllerUITests.swift */, 9518E3C21DED03510008CC46 /* UITestCase.swift */, ); path = "IGListKitExamples-UITests"; sourceTree = ""; }; 986FB7141DBBA60900A65C18 /* IGListKitMessageExample */ = { isa = PBXGroup; children = ( 986FB71A1DBBA60900A65C18 /* Assets.xcassets */, 986FB71C1DBBA60900A65C18 /* Info.plist */, 986FB72A1DBBAC8500A65C18 /* ViewControllers */, ); path = IGListKitMessageExample; sourceTree = ""; }; 986FB72A1DBBAC8500A65C18 /* ViewControllers */ = { isa = PBXGroup; children = ( 986FB7151DBBA60900A65C18 /* MessagesViewController.swift */, ); name = ViewControllers; sourceTree = ""; }; 986FB7321DBBAD8600A65C18 /* IGListKitTodayExample */ = { isa = PBXGroup; children = ( 986FB7381DBBAD8600A65C18 /* Info.plist */, 986FB73F1DBBADE400A65C18 /* ViewControllers */, ); path = IGListKitTodayExample; sourceTree = ""; }; 986FB73F1DBBADE400A65C18 /* ViewControllers */ = { isa = PBXGroup; children = ( 986FB7331DBBAD8600A65C18 /* TodayViewController.swift */, ); name = ViewControllers; sourceTree = ""; }; 9DB684DF251B1093002023DD /* Extensions */ = { isa = PBXGroup; children = ( 9DB684E0251B10C2002023DD /* UIColor+Extension.swift */, 9DFF4D05251F08F50048474A /* UIActivityIndicatorView+Extension.swift */, ); path = Extensions; sourceTree = ""; }; BAB336E12760EC95007B4098 /* Packages */ = { isa = PBXGroup; children = ( BAB336E22760EC95007B4098 /* IGListKit */, ); name = Packages; sourceTree = ""; }; EB02899E202A11DF007E17D5 /* RemodelGeneratedModels */ = { isa = PBXGroup; children = ( EB0289A0202A11DF007E17D5 /* PersonModel.value */, EB0289A1202A11DF007E17D5 /* PersonModel.h */, EB02899F202A11DF007E17D5 /* PersonModel.m */, ); path = RemodelGeneratedModels; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 2961B3891D68B031001C9451 /* IGListKitExamples */ = { isa = PBXNativeTarget; buildConfigurationList = 2961B39C1D68B031001C9451 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */; buildPhases = ( 2961B3861D68B031001C9451 /* Sources */, 2961B3871D68B031001C9451 /* Frameworks */, 2961B3881D68B031001C9451 /* Resources */, 986FB7231DBBA60900A65C18 /* Embed Foundation Extensions */, DC1296C41EC38A0B0043990F /* SwiftLint */, ); buildRules = ( ); dependencies = ( 986FB71E1DBBA60900A65C18 /* PBXTargetDependency */, 986FB73A1DBBAD8600A65C18 /* PBXTargetDependency */, ); name = IGListKitExamples; packageProductDependencies = ( BAB336E32760ECC1007B4098 /* IGListDiffKit */, BAB336E52760ECC1007B4098 /* IGListKit */, BAB336E72760ECC1007B4098 /* IGListSwiftKit */, ); productName = IGListKitExamples; productReference = 2961B38A1D68B031001C9451 /* IGListKitExamples.app */; productType = "com.apple.product-type.application"; }; 95F7F9101DE5006B00A64FEE /* IGListKitExamples-UITests */ = { isa = PBXNativeTarget; buildConfigurationList = 95F7F91A1DE5006C00A64FEE /* Build configuration list for PBXNativeTarget "IGListKitExamples-UITests" */; buildPhases = ( 95F7F90D1DE5006B00A64FEE /* Sources */, 95F7F90E1DE5006B00A64FEE /* Frameworks */, 95F7F90F1DE5006B00A64FEE /* Resources */, ); buildRules = ( ); dependencies = ( 95F7F9171DE5006C00A64FEE /* PBXTargetDependency */, ); name = "IGListKitExamples-UITests"; productName = "IGListKitExamples-UITests"; productReference = 95F7F9111DE5006B00A64FEE /* IGListKitExamples-UITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 986FB7101DBBA60900A65C18 /* IGListKitMessageExample */ = { isa = PBXNativeTarget; buildConfigurationList = 986FB7221DBBA60900A65C18 /* Build configuration list for PBXNativeTarget "IGListKitMessageExample" */; buildPhases = ( 986FB70D1DBBA60900A65C18 /* Sources */, 986FB70E1DBBA60900A65C18 /* Frameworks */, 986FB70F1DBBA60900A65C18 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = IGListKitMessageExample; packageProductDependencies = ( BAB336EB2760ECFE007B4098 /* IGListKit */, BAB336F12760ED55007B4098 /* IGListDiffKit */, BAB336F32760EDCB007B4098 /* IGListSwiftKit */, ); productName = MessageExtension; productReference = 986FB7111DBBA60900A65C18 /* IGListKitMessageExample.appex */; productType = "com.apple.product-type.app-extension.messages"; }; 986FB72E1DBBAD8600A65C18 /* IGListKitTodayExample */ = { isa = PBXNativeTarget; buildConfigurationList = 986FB73C1DBBAD8600A65C18 /* Build configuration list for PBXNativeTarget "IGListKitTodayExample" */; buildPhases = ( 986FB72B1DBBAD8600A65C18 /* Sources */, 986FB72C1DBBAD8600A65C18 /* Frameworks */, 986FB72D1DBBAD8600A65C18 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = IGListKitTodayExample; packageProductDependencies = ( BAB336E92760ECE5007B4098 /* IGListKit */, BAB336ED2760ED0C007B4098 /* IGListSwiftKit */, BAB336EF2760ED28007B4098 /* IGListDiffKit */, ); productName = IGListKitTodayExample; productReference = 986FB72F1DBBAD8600A65C18 /* IGListKitTodayExample.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 2961B3821D68B031001C9451 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0810; LastUpgradeCheck = 1500; ORGANIZATIONNAME = Instagram; TargetAttributes = { 2961B3891D68B031001C9451 = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1120; }; 95F7F9101DE5006B00A64FEE = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1120; TestTargetID = 2961B3891D68B031001C9451; }; 986FB7101DBBA60900A65C18 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1120; }; 986FB72E1DBBAD8600A65C18 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1120; }; }; }; buildConfigurationList = 2961B3851D68B031001C9451 /* Build configuration list for PBXProject "IGListKitExamples" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 2961B3811D68B031001C9451; productRefGroup = 2961B38B1D68B031001C9451 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 2961B3891D68B031001C9451 /* IGListKitExamples */, 986FB7101DBBA60900A65C18 /* IGListKitMessageExample */, 986FB72E1DBBAD8600A65C18 /* IGListKitTodayExample */, 95F7F9101DE5006B00A64FEE /* IGListKitExamples-UITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 2961B3881D68B031001C9451 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 29C629811DCFDAF3004A5BB1 /* UserHeaderView.xib in Resources */, 296DD75B1DD217C000206780 /* NibSelfSizingCell.xib in Resources */, FF73FF9E1FBA4B5500F2AB06 /* UserFooterView.xib in Resources */, 2961B3981D68B031001C9451 /* LaunchScreen.storyboard in Resources */, 26271C921DAE9EFC0073E116 /* NibCell.xib in Resources */, EB0289A3202A11E0007E17D5 /* PersonModel.value in Resources */, 2961B3951D68B031001C9451 /* Assets.xcassets in Resources */, 98B4DBF41DC2937A002BA58A /* Demo.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 95F7F90F1DE5006B00A64FEE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 986FB70F1DBBA60900A65C18 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 986FB71B1DBBA60900A65C18 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 986FB72D1DBBAD8600A65C18 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ DC1296C41EC38A0B0043990F /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = SwiftLint; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "../../scripts/lint.sh\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 2961B3861D68B031001C9451 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 299068281D75BFEC00A62888 /* MixedDataViewController.swift in Sources */, 299B54001D6BD6630074A202 /* SearchViewController.swift in Sources */, 292658661E74A49D0041B56D /* CalendarDayCell.swift in Sources */, 2961B3AE1D68B0B5001C9451 /* SpinnerCell.swift in Sources */, 292658591E749F820041B56D /* Month.swift in Sources */, 296DD7591DD2174200206780 /* NibSelfSizingCell.swift in Sources */, 296DD7531DD2147500206780 /* SelfSizingCellsViewController.swift in Sources */, 56C05B691E49B2120026DB39 /* ObjcDemoViewController.m in Sources */, 2961B3B01D68B28E001C9451 /* SearchCell.swift in Sources */, 2942FF8C1D9F39E00015D24B /* DemoSectionController.swift in Sources */, 576D20162B2CF1A50012C5B8 /* HorizontalCardsSection.swift in Sources */, 2981BA351DB868A500A987F9 /* ImageCell.swift in Sources */, 7D9DC0F82DC379A000BFB68C /* PostSectionControllerDelegate.swift in Sources */, 2942FF931D9F39E00015D24B /* SearchSectionController.swift in Sources */, 7D4506E02DC0020C0029D095 /* LoadingSectionController.swift in Sources */, 7D4506E22DC0020C0029D095 /* PostSectionController.swift in Sources */, 297546FA1DD25384002A6F89 /* FullWidthSelfSizingCell.swift in Sources */, 292658681E74AE8A0041B56D /* MonthTitleViewModel.swift in Sources */, 576D20102B2CEC4E0012C5B8 /* GridComposableSectionController.swift in Sources */, EB0289AC202A1705007E17D5 /* PersonCell.m in Sources */, 82D91B691DBA0EF300E62758 /* SingleSectionStoryboardViewController.swift in Sources */, 29D2E4AF1DD69C0E00CD255D /* DisplayViewController.swift in Sources */, 4119B2AD2D89BBF000D397BC /* ActivityComposableSectionController.swift in Sources */, 2942FF911D9F39E00015D24B /* LabelSectionController.swift in Sources */, 295D8A9D1E92ECDE001F7C06 /* Post.m in Sources */, 29F7E2AD1E92843A00197586 /* IncrementAnnouncer.swift in Sources */, 2981BA391DB874BB00A987F9 /* WorkingRangeViewController.swift in Sources */, 576D20072B2CB50E0012C5B8 /* CompositionLayoutViewController.swift in Sources */, 29C629831DCFDB57004A5BB1 /* UserHeaderView.swift in Sources */, 2961B3AC1D68B0B5001C9451 /* LoadMoreViewController.swift in Sources */, 26271C941DAE9F050073E116 /* NibCell.swift in Sources */, 7D4506D22DC001670029D095 /* FeedViewController.swift in Sources */, 2991F9191D7BADC900B0C58F /* CenterLabelCell.swift in Sources */, 7D4506D82DC001DD0029D095 /* APIService.swift in Sources */, 295D8A9A1E92EC96001F7C06 /* PostSectionController.m in Sources */, 4119B2AB2D89BB1E00D397BC /* ActivityItem.swift in Sources */, 29628F141D91905A0026B15A /* DetailLabelCell.swift in Sources */, 13DF01681F9D9CBD0092A320 /* ReorderableSectionController.swift in Sources */, 56C05B751E49B33C0026DB39 /* InteractiveCell.m in Sources */, 2991F9301D7BC0E400B0C58F /* EmptyViewController.swift in Sources */, EB0289A9202A1507007E17D5 /* PersonSectionController.m in Sources */, 29F7E2AF1E92858500197586 /* ListeningSectionController.swift in Sources */, FF73FF9C1FBA4B4400F2AB06 /* UserFooterView.swift in Sources */, 29D2E4AD1DD69B6000CD255D /* DisplaySectionController.swift in Sources */, EB0289A6202A14E1007E17D5 /* ObjcGeneratedModelDemoViewController.m in Sources */, 2991F91E1D7BB30C00B0C58F /* User.swift in Sources */, 2991F9241D7BB89F00B0C58F /* NestedAdapterViewController.swift in Sources */, 9DFF4D06251F08F50048474A /* UIActivityIndicatorView+Extension.swift in Sources */, 2961B38E1D68B031001C9451 /* AppDelegate.swift in Sources */, 2942FF941D9F39E00015D24B /* UserSectionController.swift in Sources */, 29459C001DBE48E200F05375 /* DiffTableViewController.swift in Sources */, 2991F92C1D7BBE5400B0C58F /* RemoveCell.swift in Sources */, 2942FF8D1D9F39E00015D24B /* EmbeddedSectionController.swift in Sources */, 29C6297B1DCFD857004A5BB1 /* SupplementaryViewController.swift in Sources */, 296DD7551DD2150600206780 /* SelfSizingSectionController.swift in Sources */, 2991F9281D7BB9EC00B0C58F /* EmbeddedCollectionViewCell.swift in Sources */, 13DF016A1F9D9F600092A320 /* ReorderableViewController.swift in Sources */, 576D20142B2CF15C0012C5B8 /* GridItem.swift in Sources */, 576D20182B2CF24F0012C5B8 /* HorizontalComposableSectionController.swift in Sources */, 2942FF8F1D9F39E00015D24B /* GridSectionController.swift in Sources */, 821BC4B81DB8B48300172ED0 /* StoryboardCell.swift in Sources */, 56C05B781E49B3A50026DB39 /* PhotoCell.m in Sources */, 7D4506DB2DC001FC0029D095 /* PostCell.swift in Sources */, 7D4506DC2DC001FC0029D095 /* LoadingCell.swift in Sources */, 7D4506D52DC001C40029D095 /* PostModel.swift in Sources */, 7D4506D62DC001C40029D095 /* LoadingCellModel.swift in Sources */, 576D201A2B2CF82C0012C5B8 /* SwipeActionSection.swift in Sources */, 2942FF921D9F39E00015D24B /* RemoveSectionController.swift in Sources */, 26271C8E1DAE9D3F0073E116 /* SingleSectionViewController.swift in Sources */, 2961B3AD1D68B0B5001C9451 /* LabelCell.swift in Sources */, 2942FF901D9F39E00015D24B /* HorizontalSectionController.swift in Sources */, 292658641E74A2550041B56D /* MonthSectionController.swift in Sources */, 576D200E2B2CE9EC0012C5B8 /* ExpandableComposableSectionController.swift in Sources */, 2981BA371DB869FF00A987F9 /* WorkingRangeSectionController.swift in Sources */, EB0289A2202A11E0007E17D5 /* PersonModel.m in Sources */, 292658601E74A19F0041B56D /* DayViewModel.swift in Sources */, 2961B3AB1D68B0B5001C9451 /* DemosViewController.swift in Sources */, 576D20122B2CF04F0012C5B8 /* UserComposableSectionController.swift in Sources */, 296DD75D1DD21ADA00206780 /* SelectionModel.swift in Sources */, 29C6297D1DCFD8E5004A5BB1 /* FeedItem.swift in Sources */, 56C05B6C1E49B2E80026DB39 /* UserInfoCell.m in Sources */, 2926586A1E74AEFE0041B56D /* MonthTitleCell.swift in Sources */, 821BC4B61DB8B3DC00172ED0 /* StoryboardViewController.swift in Sources */, 296DD7571DD2163800206780 /* ManuallySelfSizingCell.swift in Sources */, 292658571E749EDC0041B56D /* CalendarViewController.swift in Sources */, 29C6297F1DCFD9E9004A5BB1 /* FeedItemSectionController.swift in Sources */, 576D20092B2CB6C20012C5B8 /* SwipeActionComposabelSectionController.swift in Sources */, 2942FF8E1D9F39E00015D24B /* ExpandableSectionController.swift in Sources */, 29F7E2AA1E9283FF00197586 /* AnnouncingDepsViewController.swift in Sources */, 9DB684E1251B10C2002023DD /* UIColor+Extension.swift in Sources */, 56C05B721E49B32A0026DB39 /* CommentCell.m in Sources */, 576D200B2B2CC6DF0012C5B8 /* CompositionLayoutCell.swift in Sources */, 41A162952D8A0C82002A5A8F /* SelectionComposableSectionController.swift in Sources */, 821BC4BA1DB8B61200172ED0 /* StoryboardLabelSectionController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 95F7F90D1DE5006B00A64FEE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 9518E3C51DED057E0008CC46 /* LoadMoreViewControllerUITests.swift in Sources */, 9518E3CB1DED27840008CC46 /* DemosViewControllerUITests.swift in Sources */, 9518E3C91DED1BCB0008CC46 /* MixedDataViewControllerUITests.swift in Sources */, 7D4506E62DC0052C0029D095 /* FeedViewController.swift in Sources */, 9518E3C71DED18370008CC46 /* SearchViewControllerUITests.swift in Sources */, 9518E3C31DED03520008CC46 /* UITestCase.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 986FB70D1DBBA60900A65C18 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 986FB7281DBBA9B200A65C18 /* LabelSectionController.swift in Sources */, 986FB7291DBBA9C900A65C18 /* LabelCell.swift in Sources */, 986FB7161DBBA60900A65C18 /* MessagesViewController.swift in Sources */, 9DFF4CFB251F089E0048474A /* UIColor+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 986FB72B1DBBAD8600A65C18 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 986FB7401DBBAEA600A65C18 /* LabelSectionController.swift in Sources */, 986FB7411DBBAEA900A65C18 /* LabelCell.swift in Sources */, 986FB7341DBBAD8600A65C18 /* TodayViewController.swift in Sources */, 9DFF4D00251F089F0048474A /* UIColor+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 95F7F9171DE5006C00A64FEE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2961B3891D68B031001C9451 /* IGListKitExamples */; targetProxy = 95F7F9161DE5006C00A64FEE /* PBXContainerItemProxy */; }; 986FB71E1DBBA60900A65C18 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; target = 986FB7101DBBA60900A65C18 /* IGListKitMessageExample */; targetProxy = 986FB71D1DBBA60900A65C18 /* PBXContainerItemProxy */; }; 986FB73A1DBBAD8600A65C18 /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; target = 986FB72E1DBBAD8600A65C18 /* IGListKitTodayExample */; targetProxy = 986FB7391DBBAD8600A65C18 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 2961B3961D68B031001C9451 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 2961B3971D68B031001C9451 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 2961B39A1D68B031001C9451 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); 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 = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 2961B39B1D68B031001C9451 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; 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 = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 2961B39D1D68B031001C9451 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IGListKitExamples/IGListKitExamples.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "IGListKitExamples/ViewControllers/IGListKitExamples-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 2961B39E1D68B031001C9451 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IGListKitExamples/IGListKitExamples.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "IGListKitExamples/ViewControllers/IGListKitExamples-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 95F7F9181DE5006C00A64FEE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "IGListKitExamples-UITests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListKitExamples-UITests"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = IGListKitExamples; }; name = Debug; }; 95F7F9191DE5006C00A64FEE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "IGListKitExamples-UITests/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListKitExamples-UITests"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TEST_TARGET_NAME = IGListKitExamples; }; name = Release; }; 986FB7201DBBA60900A65C18 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/IGListKitMessageExample/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples.IGListKitMessageExample; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; }; name = Debug; }; 986FB7211DBBA60900A65C18 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/IGListKitMessageExample/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples.IGListKitMessageExample; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; }; name = Release; }; 986FB73D1DBBAD8600A65C18 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitTodayExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples.IGListKitTodayExample; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; }; name = Debug; }; 986FB73E1DBBAD8600A65C18 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitTodayExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 10.13; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples.IGListKitTodayExample; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 2961B3851D68B031001C9451 /* Build configuration list for PBXProject "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 2961B39A1D68B031001C9451 /* Debug */, 2961B39B1D68B031001C9451 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2961B39C1D68B031001C9451 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 2961B39D1D68B031001C9451 /* Debug */, 2961B39E1D68B031001C9451 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 95F7F91A1DE5006C00A64FEE /* Build configuration list for PBXNativeTarget "IGListKitExamples-UITests" */ = { isa = XCConfigurationList; buildConfigurations = ( 95F7F9181DE5006C00A64FEE /* Debug */, 95F7F9191DE5006C00A64FEE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 986FB7221DBBA60900A65C18 /* Build configuration list for PBXNativeTarget "IGListKitMessageExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 986FB7201DBBA60900A65C18 /* Debug */, 986FB7211DBBA60900A65C18 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 986FB73C1DBBAD8600A65C18 /* Build configuration list for PBXNativeTarget "IGListKitTodayExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 986FB73D1DBBAD8600A65C18 /* Debug */, 986FB73E1DBBAD8600A65C18 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ BAB336E32760ECC1007B4098 /* IGListDiffKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListDiffKit; }; BAB336E52760ECC1007B4098 /* IGListKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListKit; }; BAB336E72760ECC1007B4098 /* IGListSwiftKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListSwiftKit; }; BAB336E92760ECE5007B4098 /* IGListKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListKit; }; BAB336EB2760ECFE007B4098 /* IGListKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListKit; }; BAB336ED2760ED0C007B4098 /* IGListSwiftKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListSwiftKit; }; BAB336EF2760ED28007B4098 /* IGListDiffKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListDiffKit; }; BAB336F12760ED55007B4098 /* IGListDiffKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListDiffKit; }; BAB336F32760EDCB007B4098 /* IGListSwiftKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListSwiftKit; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2961B3821D68B031001C9451 /* Project object */; } ================================================ FILE: Examples/Examples-iOS/IGListKitExamples.xcodeproj/xcshareddata/xcschemes/IGListKitExamples.xcscheme ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples.xcodeproj/xcshareddata/xcschemes/IGListKitMessageExample.xcscheme ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitExamples.xcodeproj/xcshareddata/xcschemes/IGListKitTodayExample.xcscheme ================================================ ================================================ FILE: Examples/Examples-iOS/IGListKitMessageExample/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-iOS/IGListKitMessageExample/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "60x45", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x45", "scale" : "3x" }, { "idiom" : "ipad", "size" : "67x50", "scale" : "2x" }, { "idiom" : "ipad", "size" : "74x55", "scale" : "2x" }, { "size" : "27x20", "idiom" : "universal", "scale" : "2x", "platform" : "ios" }, { "size" : "27x20", "idiom" : "universal", "scale" : "3x", "platform" : "ios" }, { "size" : "32x24", "idiom" : "universal", "scale" : "2x", "platform" : "ios" }, { "size" : "32x24", "idiom" : "universal", "scale" : "3x", "platform" : "ios" }, { "size" : "1024x768", "idiom" : "ios-marketing", "scale" : "1x", "platform" : "ios" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-iOS/IGListKitMessageExample/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName IGListKit CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 1.0 CFBundleVersion 1 NSExtension NSExtensionPointIdentifier com.apple.message-payload-provider NSExtensionPrincipalClass IGListKitMessageExample.MessagesViewController ================================================ FILE: Examples/Examples-iOS/IGListKitMessageExample/MessagesViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListDiffKit import IGListKit import Messages import UIKit @available(iOSApplicationExtension 10.0, *) final class MessagesViewController: MSMessagesAppViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout()) let data = "Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.".components(separatedBy: " ") override func viewDidLoad() { super.viewDidLoad() adapter.collectionView = collectionView adapter.dataSource = self view.addSubview(collectionView) } override func loadView() { view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 200)) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return LabelSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/IGListKitTodayExample/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName IGListKit CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 1.0 CFBundleVersion 1 NSExtension NSExtensionPointIdentifier com.apple.widget-extension NSExtensionPrincipalClass IGListKitTodayExample.TodayViewController ================================================ FILE: Examples/Examples-iOS/IGListKitTodayExample/TodayViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListDiffKit import IGListKit import NotificationCenter import UIKit @available(iOSApplicationExtension 10.0, *) final class TodayViewController: UIViewController, NCWidgetProviding, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout()) let data = "Maecenas faucibus mollis interdum. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit.".components(separatedBy: " ") override func viewDidLoad() { super.viewDidLoad() adapter.collectionView = collectionView adapter.dataSource = self view.addSubview(collectionView) // Enables the 'Show More' button in the widget interface extensionContext?.widgetLargestAvailableDisplayMode = .expanded } override func loadView() { view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 110)) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: NCWidgetProviding func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { preferredContentSize = maxSize } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return LabelSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-iOS/LICENSE-examples.md ================================================ Copyright (c) Meta Platforms, Inc. and affiliates. The examples provided by Facebook are 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. ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/AppDelegate.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Cocoa @NSApplicationMain final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { } func applicationWillTerminate(_ aNotification: Notification) { } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "size" : "16x16", "scale" : "1x" }, { "idiom" : "mac", "size" : "16x16", "scale" : "2x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "1x" }, { "idiom" : "mac", "size" : "32x32", "scale" : "2x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "1x" }, { "idiom" : "mac", "size" : "128x128", "scale" : "2x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "1x" }, { "idiom" : "mac", "size" : "256x256", "scale" : "2x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "1x" }, { "idiom" : "mac", "size" : "512x512", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Base.lproj/Main.storyboard ================================================ CA Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Data/users.json ================================================ [{ "name": "AMELIA" }, { "name": "OLIVIA" }, { "name": "EMILY" }, { "name": "AVA" }, { "name": "ISLA" }, { "name": "JESSICA" }, { "name": "POPPY" }, { "name": "ISABELLA" }, { "name": "SOPHIE" }, { "name": "MIA" }, { "name": "RUBY" }, { "name": "LILY" }, { "name": "GRACE" }, { "name": "EVIE" }, { "name": "SOPHIA" }, { "name": "ELLA" }, { "name": "SCARLETT" }, { "name": "CHLOE" }, { "name": "ISABELLE" }, { "name": "FREYA" }, { "name": "CHARLOTTE" }, { "name": "SIENNA" }, { "name": "DAISY" }, { "name": "PHOEBE" }, { "name": "MILLIE" }, { "name": "EVA" }, { "name": "ALICE" }, { "name": "LUCY" }, { "name": "FLORENCE" }, { "name": "SOFIA" }, { "name": "LAYLA" }, { "name": "LOLA" }, { "name": "HOLLY" }, { "name": "IMOGEN" }, { "name": "MOLLY" }, { "name": "MATILDA" }, { "name": "LILLY" }, { "name": "ROSIE" }, { "name": "ELIZABETH" }, { "name": "ERIN" }, { "name": "MAISIE" }, { "name": "LEXI" }, { "name": "ELLIE" }, { "name": "HANNAH" }, { "name": "EVELYN" }, { "name": "ABIGAIL" }, { "name": "ELSIE" }, { "name": "SUMMER" }, { "name": "MEGAN" }, { "name": "JASMINE" }, { "name": "MAYA" }, { "name": "AMELIE" }, { "name": "LACEY" }, { "name": "WILLOW" }, { "name": "EMMA" }, { "name": "BELLA" }, { "name": "ELEANOR" }, { "name": "ESME" }, { "name": "ELIZA" }, { "name": "GEORGIA" }, { "name": "HARRIET" }, { "name": "GRACIE" }, { "name": "ANNABELLE" }, { "name": "EMILIA" }, { "name": "AMBER" }, { "name": "IVY" }, { "name": "BROOKE" }, { "name": "ROSE" }, { "name": "ANNA" }, { "name": "ZARA" }, { "name": "LEAH" }, { "name": "MOLLIE" }, { "name": "MARTHA" }, { "name": "FAITH" }, { "name": "HOLLIE" }, { "name": "AMY" }, { "name": "BETHANY" }, { "name": "VIOLET" }, { "name": "KATIE" }, { "name": "MARYAM" }, { "name": "FRANCESCA" }, { "name": "JULIA" }, { "name": "MARIA" }, { "name": "DARCEY" }, { "name": "ISABEL" }, { "name": "TILLY" }, { "name": "MADDISON" }, { "name": "VICTORIA" }, { "name": "ISOBEL" }, { "name": "NIAMH" }, { "name": "SKYE" }, { "name": "MADISON" }, { "name": "DARCY" }, { "name": "AISHA" }, { "name": "BEATRICE" }, { "name": "SARAH" }, { "name": "ZOE" }, { "name": "PAIGE" }, { "name": "HEIDI" }, { "name": "LYDIA" }, { "name": "SARA" }, { "name": "OLIVER" }, { "name": "JACK" }, { "name": "HARRY" }, { "name": "JACOB" }, { "name": "CHARLIE" }, { "name": "THOMAS" }, { "name": "OSCAR" }, { "name": "WILLIAM" }, { "name": "JAMES" }, { "name": "GEORGE" }, { "name": "ALFIE" }, { "name": "JOSHUA" }, { "name": "NOAH" }, { "name": "ETHAN" }, { "name": "MUHAMMAD" }, { "name": "ARCHIE" }, { "name": "LEO" }, { "name": "HENRY" }, { "name": "JOSEPH" }, { "name": "SAMUEL" }, { "name": "RILEY" }, { "name": "DANIEL" }, { "name": "MOHAMMED" }, { "name": "ALEXANDER" }, { "name": "MAX" }, { "name": "LUCAS" }, { "name": "MASON" }, { "name": "LOGAN" }, { "name": "ISAAC" }, { "name": "BENJAMIN" }, { "name": "DYLAN" }, { "name": "JAKE" }, { "name": "EDWARD" }, { "name": "FINLEY" }, { "name": "FREDDIE" }, { "name": "HARRISON" }, { "name": "TYLER" }, { "name": "SEBASTIAN" }, { "name": "ZACHARY" }, { "name": "ADAM" }, { "name": "THEO" }, { "name": "JAYDEN" }, { "name": "ARTHUR" }, { "name": "TOBY" }, { "name": "LUKE" }, { "name": "LEWIS" }, { "name": "MATTHEW" }, { "name": "HARVEY" }, { "name": "HARLEY" }, { "name": "DAVID" }, { "name": "RYAN" }, { "name": "TOMMY" }, { "name": "MICHAEL" }, { "name": "REUBEN" }, { "name": "NATHAN" }, { "name": "BLAKE" }, { "name": "MOHAMMAD" }, { "name": "JENSON" }, { "name": "BOBBY" }, { "name": "LUCA" }, { "name": "CHARLES" }, { "name": "FRANKIE" }, { "name": "DEXTER" }, { "name": "KAI" }, { "name": "ALEX" }, { "name": "CONNOR" }, { "name": "LIAM" }, { "name": "JAMIE" }, { "name": "ELIJAH" }, { "name": "STANLEY" }, { "name": "LOUIE" }, { "name": "JUDE" }, { "name": "CALLUM" }, { "name": "HUGO" }, { "name": "LEON" }, { "name": "ELLIOT" }, { "name": "LOUIS" }, { "name": "THEODORE" }, { "name": "GABRIEL" }, { "name": "OLLIE" }, { "name": "AARON" }, { "name": "FREDERICK" }, { "name": "EVAN" }, { "name": "ELLIOTT" }, { "name": "OWEN" }, { "name": "TEDDY" }, { "name": "FINLAY" }, { "name": "CALEB" }, { "name": "IBRAHIM" }, { "name": "RONNIE" }, { "name": "FELIX" }, { "name": "AIDEN" }, { "name": "CAMERON" }, { "name": "AUSTIN" }, { "name": "KIAN" }, { "name": "RORY" }, { "name": "SETH" }, { "name": "ROBERT" }, { "name": "ALBERT" }, { "name": "SONNY" }] ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Helpers/IndexSet+Extensions.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation extension IndexSet { static var zero: IndexSet { return NSIndexSet(index: 0) as IndexSet } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Helpers/Shuffle.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation extension MutableCollection { /// Shuffles the contents of this collection. mutating func shuffle() { guard count > 1 else { return } for (firstUnshuffled, unshuffledCount) in zip(indices, stride(from: count, to: 1, by: -1)) { let distance: Int = numericCast(arc4random_uniform(numericCast(unshuffledCount))) guard distance != 0 else { continue } let shuffleIndex = index(firstUnshuffled, offsetBy: distance) self.swapAt(firstUnshuffled, shuffleIndex) } } } extension Sequence { /// Returns an array with the contents of this sequence, shuffled. var shuffled: [Iterator.Element] { var result = Array(self) result.shuffle() return result } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Helpers/UsersProvider.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation final class UsersProvider { enum UsersError: Error { case invalidData } let users: [User] init(with file: URL) throws { let data = try Data(contentsOf: file) let json = try JSONSerialization.jsonObject(with: data, options: []) guard let dicts = json as? [[String: String]] else { throw UsersError.invalidData } self.users = dicts.enumerated().compactMap { index, dict in guard let name = dict["name"] else { return nil } return User(pk: index, name: name.capitalized) }.sorted(by: { $0.name < $1.name }) } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright (c) Meta Platforms, Inc. and affiliates. NSMainStoryboardFile Main NSPrincipalClass NSApplication ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/Models/User.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation import IGListDiffKit final class User: ListDiffable { let pk: Int let name: String init(pk: Int, name: String) { self.pk = pk self.name = name } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return pk as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? User else { return false } return name == object.name } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Cocoa protocol UserCollectionViewCellDelegate: AnyObject { func itemDeleted(_ user: User) } final class UserCollectionViewCell: NSCollectionViewItem { weak var delegate: UserCollectionViewCellDelegate? @IBAction func deleteButtonClicked(_ sender: AnyObject) { guard let user = representedObject as? User else { return } delegate?.itemDeleted(user) } func bindViewModel(_ user: User) { representedObject = user textField?.stringValue = user.name } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/View/UserCollectionViewCell.xib ================================================ ================================================ FILE: Examples/Examples-macOS/IGListKitExamples/ViewControllers/UsersViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Cocoa import IGListDiffKit final class UsersViewController: NSViewController { @IBOutlet weak var collectionView: NSCollectionView! // MARK: Data var users = [User]() { didSet { computeFilteredUsers() } } var searchTerm = "" { didSet { computeFilteredUsers() } } private func computeFilteredUsers() { guard !searchTerm.isEmpty else { filteredUsers = users return } filteredUsers = users.filter({ $0.name.localizedCaseInsensitiveContains(self.searchTerm) }) } fileprivate func delete(user: User) { guard let index = self.users.firstIndex(where: { $0.pk == user.pk }) else { return } self.users.remove(at: index) } // MARK: - // MARK: Diffing var isFirstRun = true var filteredUsers = [User]() { didSet { // A crash occurs when you try to use performBatchUpdates the first time guard !isFirstRun else { collectionView.reloadData() isFirstRun = false return } // get the difference between the old array of Users and the new array of Users let diff = ListDiffPaths(fromSection: 0, toSection: 0, oldArray: oldValue, newArray: filteredUsers, option: .equality) let batchUpdates = diff.forBatchUpdates() let inserts = Set(batchUpdates.inserts) let deletes = Set(batchUpdates.deletes) let updates = Set(batchUpdates.updates) let moves = Set(batchUpdates.moves) // this difference is used here to update the collection view, but it can be used // to update collection views and other similar interface elements // this code can also be added to an extension of NSCollectionView ;) // Set the animation duration when updating the collection view NSAnimationContext.current.duration = 0.25 // Perform the updates to the collection view collectionView.animator().performBatchUpdates({ collectionView.deleteItems(at: deletes) collectionView.insertItems(at: inserts) collectionView.reloadItems(at: updates) moves.forEach { move in collectionView.moveItem(at: move.from, to: move.to) } }, completionHandler: nil) } } // MARK: - private func loadSampleUsers() { guard let file = Bundle.main.url(forResource: "users", withExtension: "json") else { return } do { self.users = try UsersProvider(with: file).users } catch { NSAlert(error: error).runModal() } } // MARK: Interface override func viewDidLoad() { super.viewDidLoad() // The view needs to be backed by a CALayer to be able to enable the collections view animations you can // enable this by selecting the view controller's view in the Interface Builder in the Core Animation section // of the View Effects inspector tab, through code you can do by view.wantsLayer = true loadSampleUsers() } override func viewDidAppear() { super.viewDidAppear() view.window?.titleVisibility = .hidden } @IBAction func shuffle(_ sender: Any?) { users = users.shuffled } @IBAction func search(_ sender: NSSearchField) { searchTerm = sender.stringValue } } extension UsersViewController: UserCollectionViewCellDelegate { func itemDeleted(_ user: User) { self.delete(user: user) } } extension UsersViewController: NSCollectionViewDelegate { } extension UsersViewController: NSCollectionViewDataSource { private struct Storyboard { static let cellIdentifier = "UserCollectionViewCell" } func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { return self.filteredUsers.count } @available(OSX 10.11, *) func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: Storyboard.cellIdentifier), for: indexPath) guard let cell = item as? UserCollectionViewCell else { return item } cell.delegate = self cell.bindViewModel(filteredUsers[indexPath.item]) return cell } } extension UsersViewController: NSCollectionViewDelegateFlowLayout { func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize { let availableWidth = collectionView.bounds.width return CGSize(width: availableWidth, height: 47) } } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 7BF95C4D1F52732200F14EFE /* UserCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */; }; 7BF95C4F1F5273A100F14EFE /* UserCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */; }; 888609091DEF38A00019A4A5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888609081DEF38A00019A4A5 /* AppDelegate.swift */; }; 8886090B1DEF38A00019A4A5 /* UsersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8886090A1DEF38A00019A4A5 /* UsersViewController.swift */; }; 8886090D1DEF38A00019A4A5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8886090C1DEF38A00019A4A5 /* Assets.xcassets */; }; 888609101DEF38A00019A4A5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8886090E1DEF38A00019A4A5 /* Main.storyboard */; }; BAB336F92760F272007B4098 /* IGListDiffKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336F82760F272007B4098 /* IGListDiffKit */; }; DD9018681E0319E40003789D /* IndexSet+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9018671E0319E40003789D /* IndexSet+Extensions.swift */; }; DD90186A1E031A3E0003789D /* Shuffle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9018691E031A3E0003789D /* Shuffle.swift */; }; DDE3D8511E030AFA00F96BE4 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE3D8501E030AFA00F96BE4 /* User.swift */; }; DDE3D8541E03117600F96BE4 /* users.json in Resources */ = {isa = PBXBuildFile; fileRef = DDE3D8531E03117600F96BE4 /* users.json */; }; DDE3D8571E0311D000F96BE4 /* UsersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE3D8561E0311D000F96BE4 /* UsersProvider.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 226CE42D2AD828C900BDBF07 /* IGListKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = IGListKit; path = ../..; sourceTree = ""; }; 63F1F74ED983018C5D607DDC /* Pods_IGListKitExamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitExamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UserCollectionViewCell.xib; sourceTree = ""; }; 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserCollectionViewCell.swift; sourceTree = ""; }; 888609051DEF38A00019A4A5 /* IGListKitExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IGListKitExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 888609081DEF38A00019A4A5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 8886090A1DEF38A00019A4A5 /* UsersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersViewController.swift; sourceTree = ""; }; 8886090C1DEF38A00019A4A5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8886090F1DEF38A00019A4A5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 888609111DEF38A00019A4A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DD9018671E0319E40003789D /* IndexSet+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IndexSet+Extensions.swift"; sourceTree = ""; }; DD9018691E031A3E0003789D /* Shuffle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shuffle.swift; sourceTree = ""; }; DDE3D8501E030AFA00F96BE4 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; DDE3D8531E03117600F96BE4 /* users.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = users.json; sourceTree = ""; }; DDE3D8561E0311D000F96BE4 /* UsersProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersProvider.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 888609021DEF38A00019A4A5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BAB336F92760F272007B4098 /* IGListDiffKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 7BF95C4B1F5272FA00F14EFE /* View */ = { isa = PBXGroup; children = ( 7BF95C4C1F52732200F14EFE /* UserCollectionViewCell.xib */, 7BF95C4E1F5273A100F14EFE /* UserCollectionViewCell.swift */, ); path = View; sourceTree = ""; }; 888608FC1DEF38A00019A4A5 = { isa = PBXGroup; children = ( 888609071DEF38A00019A4A5 /* IGListKitExamples */, BAB336F52760F109007B4098 /* Packages */, 888609061DEF38A00019A4A5 /* Products */, CC5C608957384D316A78F911 /* Frameworks */, ); sourceTree = ""; }; 888609061DEF38A00019A4A5 /* Products */ = { isa = PBXGroup; children = ( 888609051DEF38A00019A4A5 /* IGListKitExamples.app */, ); name = Products; sourceTree = ""; }; 888609071DEF38A00019A4A5 /* IGListKitExamples */ = { isa = PBXGroup; children = ( 888609081DEF38A00019A4A5 /* AppDelegate.swift */, DDE3D8551E0311AF00F96BE4 /* Helpers */, DDE3D84D1E030A8000F96BE4 /* Models */, DDE3D84F1E030A9200F96BE4 /* Resources */, 7BF95C4B1F5272FA00F14EFE /* View */, DDE3D84E1E030A8400F96BE4 /* ViewControllers */, ); path = IGListKitExamples; sourceTree = ""; }; BAB336F52760F109007B4098 /* Packages */ = { isa = PBXGroup; children = ( 226CE42D2AD828C900BDBF07 /* IGListKit */, ); name = Packages; sourceTree = ""; }; CC5C608957384D316A78F911 /* Frameworks */ = { isa = PBXGroup; children = ( 63F1F74ED983018C5D607DDC /* Pods_IGListKitExamples.framework */, ); name = Frameworks; sourceTree = ""; }; DDE3D84D1E030A8000F96BE4 /* Models */ = { isa = PBXGroup; children = ( DDE3D8501E030AFA00F96BE4 /* User.swift */, ); path = Models; sourceTree = ""; }; DDE3D84E1E030A8400F96BE4 /* ViewControllers */ = { isa = PBXGroup; children = ( 8886090A1DEF38A00019A4A5 /* UsersViewController.swift */, ); path = ViewControllers; sourceTree = ""; }; DDE3D84F1E030A9200F96BE4 /* Resources */ = { isa = PBXGroup; children = ( 8886090C1DEF38A00019A4A5 /* Assets.xcassets */, DDE3D8521E03117600F96BE4 /* Data */, 888609111DEF38A00019A4A5 /* Info.plist */, 8886090E1DEF38A00019A4A5 /* Main.storyboard */, ); name = Resources; sourceTree = ""; }; DDE3D8521E03117600F96BE4 /* Data */ = { isa = PBXGroup; children = ( DDE3D8531E03117600F96BE4 /* users.json */, ); path = Data; sourceTree = ""; }; DDE3D8551E0311AF00F96BE4 /* Helpers */ = { isa = PBXGroup; children = ( DD9018671E0319E40003789D /* IndexSet+Extensions.swift */, DD9018691E031A3E0003789D /* Shuffle.swift */, DDE3D8561E0311D000F96BE4 /* UsersProvider.swift */, ); path = Helpers; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 888609041DEF38A00019A4A5 /* IGListKitExamples */ = { isa = PBXNativeTarget; buildConfigurationList = 8886091F1DEF38A00019A4A5 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */; buildPhases = ( 888609011DEF38A00019A4A5 /* Sources */, 888609021DEF38A00019A4A5 /* Frameworks */, 888609031DEF38A00019A4A5 /* Resources */, DC1296C51EC38A1C0043990F /* SwiftLint */, ); buildRules = ( ); dependencies = ( ); name = IGListKitExamples; packageProductDependencies = ( BAB336F82760F272007B4098 /* IGListDiffKit */, ); productName = IGListKitExamples; productReference = 888609051DEF38A00019A4A5 /* IGListKitExamples.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 888608FD1DEF38A00019A4A5 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0810; LastUpgradeCheck = 1500; ORGANIZATIONNAME = Instagram; TargetAttributes = { 888609041DEF38A00019A4A5 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1120; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 888609001DEF38A00019A4A5 /* Build configuration list for PBXProject "IGListKitExamples" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 888608FC1DEF38A00019A4A5; productRefGroup = 888609061DEF38A00019A4A5 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 888609041DEF38A00019A4A5 /* IGListKitExamples */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 888609031DEF38A00019A4A5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( DDE3D8541E03117600F96BE4 /* users.json in Resources */, 7BF95C4D1F52732200F14EFE /* UserCollectionViewCell.xib in Resources */, 8886090D1DEF38A00019A4A5 /* Assets.xcassets in Resources */, 888609101DEF38A00019A4A5 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ DC1296C51EC38A1C0043990F /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = SwiftLint; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "../../scripts/lint.sh\n\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 888609011DEF38A00019A4A5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 8886090B1DEF38A00019A4A5 /* UsersViewController.swift in Sources */, DD90186A1E031A3E0003789D /* Shuffle.swift in Sources */, DD9018681E0319E40003789D /* IndexSet+Extensions.swift in Sources */, 888609091DEF38A00019A4A5 /* AppDelegate.swift in Sources */, DDE3D8511E030AFA00F96BE4 /* User.swift in Sources */, 7BF95C4F1F5273A100F14EFE /* UserCollectionViewCell.swift in Sources */, DDE3D8571E0311D000F96BE4 /* UsersProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 8886090E1DEF38A00019A4A5 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 8886090F1DEF38A00019A4A5 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 8886091D1DEF38A00019A4A5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); 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; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 8886091E1DEF38A00019A4A5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; 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; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; 888609201DEF38A00019A4A5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Debug; }; 888609211DEF38A00019A4A5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(RECOMMENDED_MACOSX_DEPLOYMENT_TARGET)"; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 888609001DEF38A00019A4A5 /* Build configuration list for PBXProject "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 8886091D1DEF38A00019A4A5 /* Debug */, 8886091E1DEF38A00019A4A5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 8886091F1DEF38A00019A4A5 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 888609201DEF38A00019A4A5 /* Debug */, 888609211DEF38A00019A4A5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ BAB336F82760F272007B4098 /* IGListDiffKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListDiffKit; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 888608FD1DEF38A00019A4A5 /* Project object */; } ================================================ FILE: Examples/Examples-macOS/IGListKitExamples.xcodeproj/xcshareddata/xcschemes/IGListKitExamples.xcscheme ================================================ ================================================ FILE: Examples/Examples-macOS/LICENSE-examples.md ================================================ Copyright (c) Meta Platforms, Inc. and affiliates. The examples provided by Facebook are 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. ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/AppDelegate.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) window?.rootViewController = UINavigationController(rootViewController: DemosViewController()) window?.makeKeyAndVisible() return true } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json ================================================ { "layers" : [ { "filename" : "Front.imagestacklayer" }, { "filename" : "Middle.imagestacklayer" }, { "filename" : "Back.imagestacklayer" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json ================================================ { "assets" : [ { "size" : "1280x768", "idiom" : "tv", "filename" : "App Icon - Large.imagestack", "role" : "primary-app-icon" }, { "size" : "400x240", "idiom" : "tv", "filename" : "App Icon - Small.imagestack", "role" : "primary-app-icon" }, { "size" : "2320x720", "idiom" : "tv", "filename" : "Top Shelf Image Wide.imageset", "role" : "top-shelf-image-wide" }, { "size" : "1920x720", "idiom" : "tv", "filename" : "Top Shelf Image.imageset", "role" : "top-shelf-image" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "tv", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Assets.xcassets/LaunchImage.launchimage/Contents.json ================================================ { "images" : [ { "orientation" : "landscape", "idiom" : "tv", "extent" : "full-screen", "minimum-system-version" : "9.0", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName IGListKit CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UIRequiredDeviceCapabilities arm64 UIUserInterfaceStyle Automatic ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Models/NSObject+IGListDiffable.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import Foundation import IGListKit extension NSObject: ListDiffable { public func diffIdentifier() -> NSObjectProtocol { return self } public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return isEqual(object) } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/SectionControllers/CarouselSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class CarouselSectionController: ListSectionController { var number: Int? override init() { super.init() self.inset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) } override func numberOfItems() -> Int { return 1 } override func sizeForItem(at index: Int) -> CGSize { let height = collectionContext?.containerSize.height ?? 0 let aspectRatio: CGFloat = 0.75 // 3:4 let width = height * aspectRatio return CGSize(width: width, height: height) } override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell = collectionContext?.dequeueReusableCell(withNibName: "CarouselCell", bundle: nil, for: self, at: index) as? CarouselCell else { fatalError() } let value = number ?? 0 cell.titleLabel.text = "#\(value + 1)" return cell } override func didUpdate(to object: Any) { number = object as? Int } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/SectionControllers/DemoSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class DemoItem: NSObject { let name: String let controllerClass: UIViewController.Type let controllerIdentifier: String? init(name: String, controllerClass: UIViewController.Type, controllerIdentifier: String? = nil) { self.name = name self.controllerClass = controllerClass self.controllerIdentifier = controllerIdentifier } } final class DemoSectionController: ListSectionController { var object: DemoItem? override init() { super.init() inset = UIEdgeInsets(top: 0, left: 50, bottom: 10, right: 0) } override func numberOfItems() -> Int { return 1 } override func sizeForItem(at index: Int) -> CGSize { let itemWidth = (collectionContext!.containerSize.width / 2) - inset.left return CGSize(width: itemWidth, height: 100) } override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell: DemoCell = collectionContext?.dequeueReusableCell(for: self, at: index) else { fatalError() } cell.label.text = object?.name return cell } override func didUpdate(to object: Any) { self.object = object as? DemoItem } override func didSelectItem(at index: Int) { if let identifier = object?.controllerIdentifier { let storyboard = UIStoryboard(name: "Demo", bundle: nil) let controller = storyboard.instantiateViewController(withIdentifier: identifier) controller.title = object?.name viewController?.navigationController?.pushViewController(controller, animated: true) } else if let controller = object?.controllerClass.init() { controller.title = object?.name viewController?.navigationController?.pushViewController(controller, animated: true) } } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/SectionControllers/HorizontalSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class HorizontalSectionController: ListSectionController, ListAdapterDataSource { var number: Int? lazy var adapter: ListAdapter = { let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self.viewController) adapter.dataSource = self return adapter }() override init() { super.init() self.inset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) } override func numberOfItems() -> Int { return 1 } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 340) } override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell: EmbeddedCollectionViewCell = collectionContext?.dequeueReusableCell( for: self, at: index ) else { fatalError() } adapter.collectionView = cell.collectionView return cell } override func didUpdate(to object: Any) { number = object as? Int } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { guard let number = number else { return [] } return (0.. ListSectionController { return CarouselSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/SectionControllers/LabelSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import IGListSwiftKit import UIKit final class LabelSectionController: ListSectionController { var object: String? override func numberOfItems() -> Int { return 1 } override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { guard let cell: LabelCell = collectionContext?.dequeueReusableCell(for: self, at: index) else { fatalError() } cell.label.text = object return cell } override func didUpdate(to object: Any) { self.object = String(describing: object) } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/ViewControllers/DemosViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class DemosViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let demos: [DemoItem] = [ DemoItem(name: "Nested Adapter", controllerClass: NestedAdapterViewController.self) ] override func viewDidLoad() { super.viewDidLoad() title = "Demo Chooser" collectionView.backgroundColor = .clear view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return demos } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { return DemoSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/ViewControllers/NestedAdapterViewController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class NestedAdapterViewController: UIViewController, ListAdapterDataSource { lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) let data: [Any] = [ "Most Recent", 10, "Recently Watched", 16, "New Arrivals", 20 ] override func viewDidLoad() { super.viewDidLoad() collectionView.backgroundColor = .clear view.addSubview(collectionView) adapter.collectionView = collectionView adapter.dataSource = self } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() collectionView.frame = view.bounds } // MARK: ListAdapterDataSource func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return data as! [ListDiffable] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if object is Int { return HorizontalSectionController() } return LabelSectionController() } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Views/CarouselCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class CarouselCell: UICollectionViewCell { @IBOutlet weak var titleLabel: UILabel! override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { let normalColor = UIColor(red: 174 / 255.0, green: 198 / 255.0, blue: 207 / 255.0, alpha: 1) let focusColor = UIColor(red: 117 / 255.0, green: 155 / 255.0, blue: 169 / 255.0, alpha: 1) backgroundColor = isFocused ? focusColor : normalColor } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Views/CarouselCell.xib ================================================ ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Views/DemoCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class DemoCell: UICollectionViewCell { lazy var label: UILabel = { let view = UILabel() view.textColor = .black view.font = .boldSystemFont(ofSize: 35) self.contentView.addSubview(view) return view }() override func layoutSubviews() { super.layoutSubviews() contentView.backgroundColor = UIColor.white.withAlphaComponent(0.3) label.frame = contentView.bounds.insetBy(dx: 32, dy: 16) } override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { let newBackgroundOpacity: CGFloat = isFocused ? 0.6 : 0.3 let newFontSize: CGFloat = isFocused ? 50 : 35 contentView.backgroundColor = UIColor.white.withAlphaComponent(newBackgroundOpacity) label.font = .boldSystemFont(ofSize: newFontSize) } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Views/EmbeddedCollectionViewCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit final class EmbeddedCollectionViewCell: UICollectionViewCell { lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal let view = UICollectionView(frame: .zero, collectionViewLayout: layout) view.backgroundColor = .clear view.alwaysBounceVertical = false view.alwaysBounceHorizontal = true self.contentView.addSubview(view) return view }() override func layoutSubviews() { super.layoutSubviews() collectionView.frame = contentView.frame } override var canBecomeFocused: Bool { return false } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples/Views/LabelCell.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import UIKit final class LabelCell: UICollectionViewCell { fileprivate static let insets = UIEdgeInsets(top: 8, left: 15, bottom: 8, right: 15) fileprivate static let font = UIFont.systemFont(ofSize: 40) static var singleLineHeight: CGFloat { return font.lineHeight + insets.top + insets.bottom } static func textHeight(_ text: String, width: CGFloat) -> CGFloat { let constrainedSize = CGSize(width: width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude) let attributes = [ NSAttributedString.Key.font: font ] let options: NSStringDrawingOptions = [.usesFontLeading, .usesLineFragmentOrigin] let bounds = (text as NSString).boundingRect(with: constrainedSize, options: options, attributes: attributes, context: nil) return ceil(bounds.height) + insets.top + insets.bottom } lazy var label: UILabel = { let label = UILabel() label.backgroundColor = .clear label.numberOfLines = 1 label.font = LabelCell.font self.contentView.addSubview(label) return label }() lazy var separator: CALayer = { let layer = CALayer() layer.backgroundColor = UIColor(red: 200 / 255.0, green: 199 / 255.0, blue: 204 / 255.0, alpha: 1).cgColor self.contentView.layer.addSublayer(layer) return layer }() override func layoutSubviews() { super.layoutSubviews() let bounds = contentView.bounds label.frame = bounds.inset(by: LabelCell.insets) let height: CGFloat = 0.5 let left = LabelCell.insets.left separator.frame = CGRect(x: left, y: bounds.height - height, width: bounds.width - left, height: height) } override var canBecomeFocused: Bool { return false } } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 298D91C51DDF81DB00E70CAD /* NSObject+IGListDiffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 298D91C41DDF81DB00E70CAD /* NSObject+IGListDiffable.swift */; }; 98ED5C1B1DC52AD400EA56B2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C1A1DC52AD400EA56B2 /* AppDelegate.swift */; }; 98ED5C1D1DC52AD400EA56B2 /* DemosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C1C1DC52AD400EA56B2 /* DemosViewController.swift */; }; 98ED5C221DC52AD400EA56B2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 98ED5C211DC52AD400EA56B2 /* Assets.xcassets */; }; 98ED5C2D1DC52E7F00EA56B2 /* DemoSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C2C1DC52E7F00EA56B2 /* DemoSectionController.swift */; }; 98ED5C2F1DC52F3B00EA56B2 /* DemoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C2E1DC52F3B00EA56B2 /* DemoCell.swift */; }; 98ED5C321DC53B4E00EA56B2 /* CarouselCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C301DC53B4E00EA56B2 /* CarouselCell.swift */; }; 98ED5C331DC53B4E00EA56B2 /* CarouselCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98ED5C311DC53B4E00EA56B2 /* CarouselCell.xib */; }; 98ED5C351DC53C4000EA56B2 /* NestedAdapterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C341DC53C4000EA56B2 /* NestedAdapterViewController.swift */; }; 98ED5C371DC53CEE00EA56B2 /* LabelSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C361DC53CEE00EA56B2 /* LabelSectionController.swift */; }; 98ED5C391DC53D0B00EA56B2 /* LabelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C381DC53D0B00EA56B2 /* LabelCell.swift */; }; 98ED5C3B1DC53DD000EA56B2 /* HorizontalSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C3A1DC53DD000EA56B2 /* HorizontalSectionController.swift */; }; 98ED5C3D1DC53E5600EA56B2 /* EmbeddedCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C3C1DC53E5600EA56B2 /* EmbeddedCollectionViewCell.swift */; }; 98ED5C3F1DC53EB200EA56B2 /* CarouselSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98ED5C3E1DC53EB200EA56B2 /* CarouselSectionController.swift */; }; BAB336FD2760F338007B4098 /* IGListDiffKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336FC2760F338007B4098 /* IGListDiffKit */; }; BAB336FF2760F338007B4098 /* IGListKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB336FE2760F338007B4098 /* IGListKit */; }; BAB337012760F338007B4098 /* IGListSwiftKit in Frameworks */ = {isa = PBXBuildFile; productRef = BAB337002760F338007B4098 /* IGListSwiftKit */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 298D91C41DDF81DB00E70CAD /* NSObject+IGListDiffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSObject+IGListDiffable.swift"; sourceTree = ""; }; 98ED5C171DC52AD400EA56B2 /* IGListKitExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IGListKitExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 98ED5C1A1DC52AD400EA56B2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 98ED5C1C1DC52AD400EA56B2 /* DemosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DemosViewController.swift; path = ViewControllers/DemosViewController.swift; sourceTree = ""; }; 98ED5C211DC52AD400EA56B2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 98ED5C231DC52AD400EA56B2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 98ED5C2C1DC52E7F00EA56B2 /* DemoSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DemoSectionController.swift; path = SectionControllers/DemoSectionController.swift; sourceTree = ""; }; 98ED5C2E1DC52F3B00EA56B2 /* DemoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DemoCell.swift; path = Views/DemoCell.swift; sourceTree = ""; }; 98ED5C301DC53B4E00EA56B2 /* CarouselCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CarouselCell.swift; path = Views/CarouselCell.swift; sourceTree = ""; }; 98ED5C311DC53B4E00EA56B2 /* CarouselCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = CarouselCell.xib; path = Views/CarouselCell.xib; sourceTree = ""; }; 98ED5C341DC53C4000EA56B2 /* NestedAdapterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NestedAdapterViewController.swift; path = ViewControllers/NestedAdapterViewController.swift; sourceTree = ""; }; 98ED5C361DC53CEE00EA56B2 /* LabelSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LabelSectionController.swift; path = SectionControllers/LabelSectionController.swift; sourceTree = ""; }; 98ED5C381DC53D0B00EA56B2 /* LabelCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LabelCell.swift; path = Views/LabelCell.swift; sourceTree = ""; }; 98ED5C3A1DC53DD000EA56B2 /* HorizontalSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HorizontalSectionController.swift; path = SectionControllers/HorizontalSectionController.swift; sourceTree = ""; }; 98ED5C3C1DC53E5600EA56B2 /* EmbeddedCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EmbeddedCollectionViewCell.swift; path = Views/EmbeddedCollectionViewCell.swift; sourceTree = ""; }; 98ED5C3E1DC53EB200EA56B2 /* CarouselSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CarouselSectionController.swift; path = SectionControllers/CarouselSectionController.swift; sourceTree = ""; }; BAB336FB2760F32A007B4098 /* IGListKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = IGListKit; path = ../..; sourceTree = ""; }; C7DB43F76EDA298E2CADE284 /* Pods_IGListKitExamples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_IGListKitExamples.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 98ED5C141DC52AD400EA56B2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( BAB336FD2760F338007B4098 /* IGListDiffKit in Frameworks */, BAB336FF2760F338007B4098 /* IGListKit in Frameworks */, BAB337012760F338007B4098 /* IGListSwiftKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 25284CDF3BDBCCE297C9E448 /* Frameworks */ = { isa = PBXGroup; children = ( C7DB43F76EDA298E2CADE284 /* Pods_IGListKitExamples.framework */, ); name = Frameworks; sourceTree = ""; }; 298D91C31DDF81DB00E70CAD /* Models */ = { isa = PBXGroup; children = ( 298D91C41DDF81DB00E70CAD /* NSObject+IGListDiffable.swift */, ); path = Models; sourceTree = ""; }; 98ED5C0E1DC52AD300EA56B2 = { isa = PBXGroup; children = ( 98ED5C191DC52AD400EA56B2 /* IGListKitExamples */, BAB336FA2760F32A007B4098 /* Packages */, 98ED5C181DC52AD400EA56B2 /* Products */, 25284CDF3BDBCCE297C9E448 /* Frameworks */, ); sourceTree = ""; }; 98ED5C181DC52AD400EA56B2 /* Products */ = { isa = PBXGroup; children = ( 98ED5C171DC52AD400EA56B2 /* IGListKitExamples.app */, ); name = Products; sourceTree = ""; }; 98ED5C191DC52AD400EA56B2 /* IGListKitExamples */ = { isa = PBXGroup; children = ( 98ED5C1A1DC52AD400EA56B2 /* AppDelegate.swift */, 98ED5C211DC52AD400EA56B2 /* Assets.xcassets */, 98ED5C231DC52AD400EA56B2 /* Info.plist */, 298D91C31DDF81DB00E70CAD /* Models */, 98ED5C2B1DC52E6200EA56B2 /* SectionControllers */, 98ED5C291DC52B2400EA56B2 /* ViewControllers */, 98ED5C2A1DC52B2900EA56B2 /* Views */, ); path = IGListKitExamples; sourceTree = ""; }; 98ED5C291DC52B2400EA56B2 /* ViewControllers */ = { isa = PBXGroup; children = ( 98ED5C1C1DC52AD400EA56B2 /* DemosViewController.swift */, 98ED5C341DC53C4000EA56B2 /* NestedAdapterViewController.swift */, ); name = ViewControllers; sourceTree = ""; }; 98ED5C2A1DC52B2900EA56B2 /* Views */ = { isa = PBXGroup; children = ( 98ED5C301DC53B4E00EA56B2 /* CarouselCell.swift */, 98ED5C311DC53B4E00EA56B2 /* CarouselCell.xib */, 98ED5C2E1DC52F3B00EA56B2 /* DemoCell.swift */, 98ED5C3C1DC53E5600EA56B2 /* EmbeddedCollectionViewCell.swift */, 98ED5C381DC53D0B00EA56B2 /* LabelCell.swift */, ); name = Views; sourceTree = ""; }; 98ED5C2B1DC52E6200EA56B2 /* SectionControllers */ = { isa = PBXGroup; children = ( 98ED5C3E1DC53EB200EA56B2 /* CarouselSectionController.swift */, 98ED5C2C1DC52E7F00EA56B2 /* DemoSectionController.swift */, 98ED5C3A1DC53DD000EA56B2 /* HorizontalSectionController.swift */, 98ED5C361DC53CEE00EA56B2 /* LabelSectionController.swift */, ); name = SectionControllers; sourceTree = ""; }; BAB336FA2760F32A007B4098 /* Packages */ = { isa = PBXGroup; children = ( BAB336FB2760F32A007B4098 /* IGListKit */, ); name = Packages; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 98ED5C161DC52AD400EA56B2 /* IGListKitExamples */ = { isa = PBXNativeTarget; buildConfigurationList = 98ED5C261DC52AD400EA56B2 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */; buildPhases = ( 98ED5C131DC52AD400EA56B2 /* Sources */, 98ED5C141DC52AD400EA56B2 /* Frameworks */, 98ED5C151DC52AD400EA56B2 /* Resources */, DC1296C31EC389410043990F /* SwiftLint */, ); buildRules = ( ); dependencies = ( ); name = IGListKitExamples; packageProductDependencies = ( BAB336FC2760F338007B4098 /* IGListDiffKit */, BAB336FE2760F338007B4098 /* IGListKit */, BAB337002760F338007B4098 /* IGListSwiftKit */, ); productName = IGListKitExamples; productReference = 98ED5C171DC52AD400EA56B2 /* IGListKitExamples.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 98ED5C0F1DC52AD300EA56B2 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0810; LastUpgradeCheck = 1500; ORGANIZATIONNAME = Instagram; TargetAttributes = { 98ED5C161DC52AD400EA56B2 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1120; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 98ED5C121DC52AD300EA56B2 /* Build configuration list for PBXProject "IGListKitExamples" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 98ED5C0E1DC52AD300EA56B2; productRefGroup = 98ED5C181DC52AD400EA56B2 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 98ED5C161DC52AD400EA56B2 /* IGListKitExamples */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 98ED5C151DC52AD400EA56B2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 98ED5C221DC52AD400EA56B2 /* Assets.xcassets in Resources */, 98ED5C331DC53B4E00EA56B2 /* CarouselCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ DC1296C31EC389410043990F /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = SwiftLint; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "../../scripts/lint.sh\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 98ED5C131DC52AD400EA56B2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 298D91C51DDF81DB00E70CAD /* NSObject+IGListDiffable.swift in Sources */, 98ED5C1D1DC52AD400EA56B2 /* DemosViewController.swift in Sources */, 98ED5C1B1DC52AD400EA56B2 /* AppDelegate.swift in Sources */, 98ED5C391DC53D0B00EA56B2 /* LabelCell.swift in Sources */, 98ED5C2F1DC52F3B00EA56B2 /* DemoCell.swift in Sources */, 98ED5C3F1DC53EB200EA56B2 /* CarouselSectionController.swift in Sources */, 98ED5C2D1DC52E7F00EA56B2 /* DemoSectionController.swift in Sources */, 98ED5C321DC53B4E00EA56B2 /* CarouselCell.swift in Sources */, 98ED5C3D1DC53E5600EA56B2 /* EmbeddedCollectionViewCell.swift in Sources */, 98ED5C371DC53CEE00EA56B2 /* LabelSectionController.swift in Sources */, 98ED5C351DC53C4000EA56B2 /* NestedAdapterViewController.swift in Sources */, 98ED5C3B1DC53DD000EA56B2 /* HorizontalSectionController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 98ED5C241DC52AD400EA56B2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); 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; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; 98ED5C251DC52AD400EA56B2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; 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; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = appletvos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; }; name = Release; }; 98ED5C271DC52AD400EA56B2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Debug; }; 98ED5C281DC52AD400EA56B2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Developer"; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = IGListKitExamples/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitExamples; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 12.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 98ED5C121DC52AD300EA56B2 /* Build configuration list for PBXProject "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 98ED5C241DC52AD400EA56B2 /* Debug */, 98ED5C251DC52AD400EA56B2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 98ED5C261DC52AD400EA56B2 /* Build configuration list for PBXNativeTarget "IGListKitExamples" */ = { isa = XCConfigurationList; buildConfigurations = ( 98ED5C271DC52AD400EA56B2 /* Debug */, 98ED5C281DC52AD400EA56B2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ BAB336FC2760F338007B4098 /* IGListDiffKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListDiffKit; }; BAB336FE2760F338007B4098 /* IGListKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListKit; }; BAB337002760F338007B4098 /* IGListSwiftKit */ = { isa = XCSwiftPackageProductDependency; productName = IGListSwiftKit; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 98ED5C0F1DC52AD300EA56B2 /* Project object */; } ================================================ FILE: Examples/Examples-tvOS/IGListKitExamples.xcodeproj/xcshareddata/xcschemes/IGListKitExamples.xcscheme ================================================ ================================================ FILE: Examples/Examples-tvOS/LICENSE-examples.md ================================================ Copyright (c) Meta Platforms, Inc. and affiliates. The examples provided by Facebook are 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. ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' gem 'cocoapods', '~> 1.16.2' gem 'danger', '~> 9.3.1' gem 'danger-swiftlint', '~> 0.33.0' gem 'slather', '~> 2.7.4' gem 'xcpretty', '~> 0.3.0' ================================================ FILE: Guides/Best Practices and FAQ.md ================================================ # Best Practices and FAQs This guide provides notes and details on best practices in using `IGListKit`, general tips, and answers to FAQs. ## Best Practices - We recommend adding an assert to check [`-isKindOfClass:`](https://developer.apple.com/reference/objectivec/1418956-nsobject/1418511-iskindofclass) on the object you receive in [`-didUpdateToObject:`](https://github.com/Instagram/IGListKit/blob/main/Source/IGListSectionController.h#L63-L72) in your section controllers. This makes it easy to track down easily-overlooked mistakes in your [`IGListAdapaterDataSource`](https://instagram.github.io/IGListKit/Protocols/IGListAdapterDataSource.html#/c:objc(pl)IGListAdapterDataSource(im)listAdapter:sectionControllerForObject:) implementation. If this assert is ever hit, that means `IGListKit` has sent your section controller the incorrect type of object. This would only happen if your objects provide *non-unique* diff identifiers. ```objective-c // Objective-C - (void)didUpdateToObject:(id)object { NSParameterAssert([object isKindOfClass:[MyModelClass class]]); _myModel = object; } ``` ```swift // Swift func didUpdate(to object: Any) { precondition(object is MyModelClass) myModel = object as! MyModelClass } ``` - Make sure your [`-diffIdentifier`](https://instagram.github.io/IGListKit/Protocols/IGListDiffable.html#/c:objc(pl)IGListDiffable(im)diffIdentifier) implementation returns a **unique identifier** for each object. - We highly recommend using single-item sections when possible. That is, each section controller manages a single model (which may have one or multiple cells). This gives you the greatest amount of flexibility, modularity, and re-use for your components. ## Frequently asked questions #### I upgraded IGListKit and now everything is broken! Check out our [migration guide](https://github.com/Instagram/IGListKit/blob/main/Guides/Migration.md) to make upgrading easier. #### How do you implement separators between cells? See discussion in [#329](https://github.com/Instagram/IGListKit/issues/329) #### How do I fix the error `Could not build Objective-C module 'IGListKit'`? See discussion in [#316](https://github.com/Instagram/IGListKit/issues/316) #### The documentation and examples have `` feature or changes, but I don't have it in my version. Why? This feature is on the `main` branch only and hasn't been officially tagged and [released](https://github.com/Instagram/IGListKit/releases). If you need to, you can [install from the `main` branch](https://instagram.github.io/IGListKit/installation.html). #### Does `IGListKit` work with...? - Core Data ([Working with Core Data](https://instagram.github.io/IGListKit/working-with-core-data.html) Guide) - AsyncDisplayKit ([AsyncDisplayKit/#2942](https://github.com/facebook/AsyncDisplayKit/pull/2942)) - ComponentKit ([ocrickard/IGListKit-ComponentKit](https://github.com/ocrickard/IGListKit-ComponentKit)) - RxSwift ([RxSwiftCommunity/RxIGListKit](https://github.com/RxSwiftCommunity/RxIGListKit)) - React Native - Reactive Cocoa Yes. #### Does `IGListKit` work with `UITableView`? No, but you can install the [diffing subspec via CocoaPods](https://instagram.github.io/IGListKit/installation.html). #### What's the purpose of `IGListCollectionView`? Historically, we used this subclass to gain compile-time safety to prevent disallowed methods from being called on `UICollectionView`, because `IGListKit` handles model and view updates. However, it has since been removed. See discussion at [#409](https://github.com/Instagram/IGListKit/issues/409). #### How can I manage cell selection and deselection? See discussion at [#184](https://github.com/Instagram/IGListKit/issues/184). #### I have a *huge* data set and [`-performUpdatesAnimated: completion:`](https://instagram.github.io/IGListKit/Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)performUpdatesAnimated:completion:) is *super* slow. What do I do? If you have multiple thousands of items and you cannot batch them in, you'll see performance issues with `-performUpdatesAnimated: completion:`. The real bottleneck behind the scenes here is `UICollectionView` attempting to insert so many cells at once. Instead, call [`-reloadDataWithCompletion:`](https://instagram.github.io/IGListKit/Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)reloadDataWithCompletion:) when you first load data. Behind the scenes, this method *does not* do any diffing and simply calls `-reloadData` on `UICollectionView`. For subsequent updates, you can then use `-performUpdatesAnimated: completion:`. #### How do I use IGListKit and estimated cell sizes with Auto Layout? This should work in theory, and we have an [example section controller](https://github.com/Instagram/IGListKit/blob/main/Examples/Examples-iOS/IGListKitExamples/SectionControllers/SelfSizingSectionController.swift), but the estimated-size API in `UICollectionViewFlowLayout` has changed dramatically over different iOS versions, making first-class support in IGListKit difficult. We don't use estimated cell sizes or Auto Layout in Instagram and cannot commit to fully supporting it. See [#516](https://github.com/Instagram/IGListKit/issues/516) for a master list of all known issues. We very much welcome contribution to fixing this! #### Is creating a "wrapper" model just for IGListKit ok? Yes! We create models that act as a grab-bag for other models, specifically for use in section controllers. Things like: ```swift class WeatherSectionModel { let location: Location let forecast: Forecast let conditions: Conditions } ``` Just don't forget to make your models diffable using the data in the contained models: ```swift extension WeatherSectionModel: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return location.identifier } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? WeatherSectionModel else { return false } return location == object.location && forecast == object.forecast && conditions == object.conditions } } ``` #### What if I want to make my Swift structs diffable? Give [this box](https://github.com/Instagram/IGListKit/issues/35#issuecomment-277503724) a try. #### I want to deliver messages to certain section controllers, how do I do that? We recommend using dependency injection and announcing changes, demonstrated in [our example](https://github.com/Instagram/IGListKit/blob/main/Examples/Examples-iOS/IGListKitExamples/ViewControllers/AnnouncingDepsViewController.swift). #### Should I reuse my section controllers between models? No! `IGListKit` is designed to have a 1:1 instance mapping between objects and section controllers. `IGListKit` does not reuse section controllers, and if you do unintended behaviors will occur. `IGListKit` _does_ still use `UICollectionView`'s cell reuse, so you shouldn't be concerned about performance. #### Why does `UICollectionViewFlowLayout` put everything in a new row? `UICollectionViewFlowLayout` has its limitations, and it's not well designed to support sections on the same "line". Instead you should use [`IGListCollectionViewLayout`](https://github.com/Instagram/IGListKit/blob/main/Source/IGListCollectionViewLayout.h). #### What if I just want a section controller and don't need the object? Feel free to use a static string or number as your model. You can use this object as a "key" to find your section controller. Take a look at our [example](https://github.com/Instagram/IGListKit/blob/main/Examples/Examples-iOS/IGListKitExamples/ViewControllers/SearchViewController.swift#L34) of this. #### How do I make my cells diff and animate? Use [`IGListBindingSectionController`](https://github.com/Instagram/IGListKit/blob/main/Source/IGListBindingSectionController.h) to automatically diff and animate your cells. #### How can I power and update the number of items in a section controller with a dynamic array? We recommend creating a model that owns an array to the items that power `numberOfItems`. Checkout our [Post example](https://github.com/Instagram/IGListKit/blob/main/Examples/Examples-iOS/IGListKitExamples/SectionControllers/PostSectionController.m#L32) that has dynamic comment cells. Just be sure to check when your array changes: ```swift class Forecast: ListDiffable { let day: Date let hourly: [HourlyForecast] func diffIdentifier() -> NSObjectProtocol { return day } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard self !== object else { return true } guard let object = object as? Forecast else { return false } return hourly == object.hourly // compare elements in the arrays } } ``` ================================================ FILE: Guides/Generating your models using remodel.md ================================================ # Generating your IGListDiffable models using remodel With the `IGListDiffable` plugin for [remodel by facebook](https://github.com/facebook/remodel), you can automatically generate models conforming to the `IGListDiffable`. This will automatically implement `hash`, `isEqual:` and `description`, as well as `diffIdentifier` and `isEqualToDiffableObject:` for you. Remodel is also capable to generate additional code, like conforming to `NSCoding` or additional Builder classes for your model object. It will make creating and updating models much easier, faster and safer. In `/remodel-plugin`, you can find the source files to build the `IGListDiffable` plugin locally. ## Installation ### 1. Remodel installation Please follow the [installation instructions](https://github.com/facebook/remodel) in the main remodel repository. tl;dr: Either clone the original repository, or use an npm installation. In the latter case you can run `which remodel-gen` to find out the path of your installation. ### 2. Plugin installation Copy the following files & folders within `/remodel-plugin` into your local remodel checkout: - `/src/plugins/iglistdiffable.ts` - the actual plugin - `/src/__tests__/plugins/iglistdiffable-test.ts` - unit tests - `/features/iglistdiffable.feature` - integration tests And then register the new plugin with the system: - Edit `/remodel/src/value-object-default-config.ts` and add `iglistdiffable` to the list of basePlugins: ``` // value-object-default-config.ts basePlugins: List.of( 'assert-nullability', 'assume-nonnull', 'builder', 'coding', 'copying', 'description', 'equality', 'fetch-status', 'immutable-properties', 'init-new-unavailable', 'use-cpp', 'iglistdiffable' ) ``` ### 3. Build plugin: Once you copied them over and registered the plugin, you have to compile the typescript files into javascript. Do do so run this command from the remodel directory: - `./bin/build` ### 4. Run tests (optional) To run the unit/integration tests, you can run the following commands: - `./bin/runUnitTests` - `./bin/runAcceptanceTests` This is especially useful if you plan to change/extend the plugin in any way. ### 5. Use the plugin Now you are ready to generate your `IGListDiffable` conforming models! To generate a model, create a new `.value` file. Here's an example: ``` # PersonModel.value PersonModel includes(IGListDiffable) { NSString *firstName NSString *lastName %diffIdentifier NSString *uniqueId } ``` To generate your Objective-C models, run the generation tool like this: `./bin/generate path/to/your/PersonModel.value` This will generate the following Objective-C files in the same directory: ``` // PersonModel.h @interface PersonModel : NSObject @property (nonatomic, readonly, copy) NSString *firstName; @property (nonatomic, readonly, copy) NSString *lastName; @property (nonatomic, readonly, copy) NSString *uniqueId; - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId; @end ``` and ``` // PersonModel.m @implementation PersonModel - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId { if ((self = [super init])) { _firstName = [firstName copy]; _lastName = [lastName copy]; _uniqueId = [uniqueId copy]; } return self; } - (id)diffIdentifier { return _uniqueId; } - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } - (BOOL)isEqual:(PersonModel *)object { if (self == object) { return YES; } else if (self == nil || object == nil || ![object isKindOfClass:[self class]]) { return NO; } return (_firstName == object->_firstName ? YES : [_firstName isEqual:object->_firstName]) && (_lastName == object->_lastName ? YES : [_lastName isEqual:object->_lastName]) && (_uniqueId == object->_uniqueId ? YES : [_uniqueId isEqual:object->_uniqueId]); } - (id)copyWithZone:(nullable NSZone *)zone { return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ - \n\t firstName: %@; \n\t lastName: %@; \n\t uniqueId: %@; \n", [super description], _firstName, _lastName, _uniqueId]; } - (NSUInteger)hash { NSUInteger subhashes[] = {[_firstName hash], [_lastName hash], [_uniqueId hash]}; NSUInteger result = subhashes[0]; for (int ii = 1; ii < 3; ++ii) { unsigned long long base = (((unsigned long long)result) << 32 | subhashes[ii]); base = (~base) + (base << 18); base ^= (base >> 31); base *= 21; base ^= (base >> 11); base += (base << 6); base ^= (base >> 22); result = base; } return result; } @end ``` ## Documentation Please see the main remodel repository for [additional documentation](https://github.com/facebook/remodel) ================================================ FILE: Guides/Getting Started.md ================================================ # Getting Started This guide provides a brief overview for how to get started using `IGListKit`. ## Creating your first list After installing `IGListKit`, creating a new list is easy. ### Creating a section controller Creating a new section controller is simple. Subclass `IGListSectionController` and override at least `cellForItemAtIndex:` and `sizeForItemAtIndex:`. Take a look at [LabelSectionController](https://raw.githubusercontent.com/Instagram/IGListKit/main/Examples/Examples-iOS/IGListKitExamples/SectionControllers/LabelSectionController.swift) for an example section controller that handles a `String` and configures a single cell with a `UILabel`. ```swift class LabelSectionController: ListSectionController { override func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } override func cellForItem(at index: Int) -> UICollectionViewCell { return collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index) } } ``` ### Creating the UI After creating at least one section controller, you must create a `UICollectionView` and `IGListAdapter`. ```swift let layout = UICollectionViewFlowLayout() let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) let updater = ListAdapterUpdater() let adapter = ListAdapter(updater: updater, viewController: self) adapter.collectionView = collectionView ``` > **Note:** This example is done within a `UIViewController` and uses both a stock `UICollectionViewFlowLayout` and `IGListAdapterUpdater`. You can use your own layout and updater if you need advanced features! ### Connecting the data source The last step is the `IGListAdapter`'s data source and returning some data. ```swift func objects(for listAdapter: ListAdapter) -> [ListDiffable] { // this can be anything! return [ "Foo", "Bar", 42, "Biz" ] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { if object is String { return LabelSectionController() } else { return NumberSectionController() } } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } ``` After you have created the data source you need to connect it to the `IGListAdapter` by setting its `dataSource` property: ```swift adapter.dataSource = self ``` You can return an array of _any_ type of data, as long as it conforms to `IGListDiffable`. ### Immutability The data should be immutable. If you return mutable objects that you will be editing later, `IGListKit` will not be able to diff the models accurately. This is because the instances have already been changed. Thus, the updates to the objects would be lost. Instead, always return a newly instantiated, immutable object and implement `IGListDiffable`. ## Diffing `IGListKit` uses an algorithm adapted from a paper titled [A technique for isolating differences between files](http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL) by Paul Heckel. This algorithm uses a technique known as the *longest common subsequence* to find a minimal diff between collections in linear time `O(n)`. It finds all **inserts**, **deletes**, **updates**, and **moves** between arrays of data. To add custom, diffable models, you need to conform to the `IGListDiffable` protocol and implement `diffIdentifier()` and `isEqual(toDiffableObject:)`. > **Note:** an object's `diffIdentifier()` should never change. If an object mutates it's `diffIdentifer()` the behavior of IGListKit is undefined (and almost assuredly undesirable). For an example, consider the following model: ```swift class User { let primaryKey: Int let name: String // implementation, etc } ``` The user's `primaryKey` uniquely identifies user data, and the `name` is just the value for that user. Let's say a server returns a `User` object that looks like this: ```swift let shayne = User(primaryKey: 2, name: "Shayne") ``` But sometime after the client receives `shayne`, someone changes their name: ```swift let ann = User(primaryKey: 2, name: "Ann") ``` Both `shayne` and `ann` represent the same *unique* data because they share the same `primaryKey`, but they are not *equal* because their names are different. To represent this in `IGListKit`'s diffing, add and implement the `IGListDiffable` protocol: ```swift extension User: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return primaryKey } func isEqual(toDiffableObject object: Any?) -> Bool { if let object = object as? User { return name == object.name } return false } } ``` The algorithm will skip updating two `User` objects that have the same `primaryKey` and `name`, even if they are different instances! You now avoid unnecessary UI updates in the collection view even when providing new instances. > **Note:** Remember that `isEqual(toDiffableObject:)` should return `false` when you want to reload the cells in the corresponding section controller. ### Diffing outside of IGListKit If you want to use the diffing algorithm outside of `IGListAdapter` and `UICollectionView`, you can! The diffing algorithm was built with the flexibility to be used with any models that conform to `IGListDiffable`. ```swift let result = ListDiff(oldArray: oldUsers, newArray: newUsers, .equality) ``` With this you have all of the deletes, reloads, moves, and inserts! There's even a function to generate `NSIndexPath` results. ## Advanced Features ### Working Range A *working range* is a range of section controllers who aren't yet visible, but are near the screen. Section controllers are notified of their entrance and exit to this range. This concept lets your section controllers **prepare content** before they come on screen (e.g. download images). The `IGListAdapter` must be initialized with a range value in order to work. This value is a multiple of the visible height or width, depending on the scroll-direction. ```swift let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 1) // 1 before/after visible objects ``` ![working-range](https://raw.githubusercontent.com/Instagram/IGListKit/main/Resources/workingrange.png) You can set the weak `workingRangeDelegate` on a section controller to receive events. ### Supplementary Views Adding supplementary views to section controllers is as simple as setting the (weak) `supplementaryViewSource` and implementing the `IGListSupplementaryViewSource` protocol. This protocol works nearly the same as returning and configuring cells. ### Display Delegate Section controllers can set the weak `displayDelegate` delegate to an object, including `self`, to receive display events about a section controller and individual cells. ### Custom Updaters The default `IGListAdapterUpdater` should handle any `UICollectionView` update that you need. However, if you find the functionality lacking, or want to perform updates in a very specific way, you can create an object that conforms to the `IGListUpdatingDelegate` protocol and initialize a new `IGListAdapter` with it. Check out the updater `IGListReloadDataUpdater` (used in unit tests) for an example. ================================================ FILE: Guides/IGListDiffable and Equality.md ================================================ # IGListDiffable and Equality This guide explains the `IGListDiffable` protocol and how to write good `-isEqual:` methods. ## Background The [`IGListDiffable` protocol](https://instagram.github.io/IGListKit/Protocols/IGListDiffable.html) requires clients to implement two methods, `-diffIdentifier` and `-isEqualToDiffableObject:`. The method `-isEqualToDiffableObject:` should perform the same type of check as `-isEqual:`, but without impacting performance characteristics, like in Objective-C containers such as `NSDictionary` and `NSSet`. Why are both of these methods required for diffing? The point of having the two methods has to do with **identity** and **equality**, where the diff identifier uniquely identifies data (common scenario is primary key in databases). Equality comes into play when comparing the values of two uniquely identical objects (driving reloading). See also: [#509](https://github.com/Instagram/IGListKit/issues/509) ## `IGListDiffable` bare minimum The quickest way to get started with diffable models is use the _object itself_ as the identifier, and use the superclass's `-[NSObject isEqual:]` implementation for equality: ```objc - (id)diffIdentifier { return self; } - (BOOL)isEqualToDiffableObject:(id)object { return [self isEqual:object]; } ``` ## Writing better Equality methods Even though `IGListKit` uses the method `-isEqualToDiffableObject:`, the concepts of writing a good equality check apply in general. Here are the basics to writing good `-isEqual:` and `-hash` functions. Note this is all Objective-C but applies to Swift also. - If you override `-isEqual:` you **must** override `-hash`. Check out this [article by Mike Ash](https://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html) for details. - Always compare the pointer first. This saves a lot of wasteful `objc_msgSend(...)` calls and value comparisons if checking the same instance. - When comparing object values, always check for `nil` before `-isEqual:`. For example, `[nil isEqual:nil]` counterintuitively returns `NO`. Instead, do `left == right || [left isEqual:right]`. - Always compare the **cheapest values first**. For example, doing `[self.array isEqual:other.array] && self.intVal == other.intVal` is extremely wasteful if the `intVal` values are different. Use lazy evaluation! As an example, if I had a `User` model with the following interface: ```objc @interface User : NSObject @property NSInteger identifier; @property NSString *name; @property NSArray *posts; @end ``` You would implement its equality methods like so: ```objc @implementation User - (NSUInteger)hash { return self.identifier; } - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[User class]]) { return NO; } User *right = object; return self.identifier == right.identifier && (self.name == right.name || [self.name isEqual:right.name]) && (self.posts == right.posts || [self.posts isEqualToArray:right.posts]); } @end ``` ## Using both `IGListDiffable` and `-isEqual:` Making your objects work universally with Objective-C containers and `IGListKit` is easy once you've implemented `-isEqual:` and `-hash`. ```objc @interface User // properties... @end @implementation User - (id)diffIdentifier { return @(self.identifier); } - (BOOL)isEqualToDiffableObject:(id)object { return [self isEqual:object]; } @end ``` ================================================ FILE: Guides/Installation.md ================================================ # Installation This guide provides details on how to install `IGListKit`. ## CocoaPods The preferred method of installation for `IGListKit` is using [CocoaPods](https://cocoapods.org/). In order to use the latest release of the framework, add the following to your `Podfile`: ```ruby pod 'IGListKit', '~> 4.0' ``` ### Using `main` Alternatively, you can use the latest version from the [`main` branch](https://github.com/Instagram/IGListKit/tree/main). This is what we use at Instagram, so you can be confident that `main` is always stable and reliable. ```ruby pod 'IGListKit', :git => 'https://github.com/Instagram/IGListKit.git', :branch => 'main' ``` > **Note:** while `main` is stable, it may have breaking changes. Before updating to `main`, be sure to check the [`CHANGELOG`](https://github.com/Instagram/IGListKit/blob/main/CHANGELOG.md) for details on changes. ### Subspecs With the exception of `macOS` (which currently only supports the diffing algorithm components), using `pod 'IGListKit'` will get you the full library, including the flexible `UICollectionView` system. Learn more about how to get started in our [Getting Started guide](https://instagram.github.io/IGListKit/getting-started.html). However, if you only want to use the diffing components of this library, then you can use the diffing subspec in your `Podfile`: ```ruby pod 'IGListKit/Diffing', '~> 4.0' ``` Regardless of whether you only use the diffing components, or the entire library, the imports are the same: ```swift import IGListKit ``` ## Carthage If using [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`: ```ogdl github "Instagram/IGListKit" ~> 4.0 ``` ================================================ FILE: Guides/Migration.md ================================================ # Migration This guide provides details for how to migrate between major versions of `IGListKit`. ## From 2.x to 3.x For details on all changes in IGListKit 3.0.0, please see the [release notes](https://github.com/Instagram/IGListKit/releases/tag/3.0.0). > **NOTE:** This release contains *a lot* of improvements and source-breaking API changes, especially for Swift clients. These are all noted in the full [release notes](https://github.com/Instagram/IGListKit/releases/tag/3.0.0). ### "IG" prefix removed for Swift We have improved how `IGListKit` APIs get imported into Swift. The `IG` prefix has been removed for Swift clients. For example, `IGListSectionController` becomes `ListSectionController` instead. Along with other interoperability improvements, this makes `IGListKit` more readable in Swift. To migrate, use Xcode's Find navigator (command-3), search for `IGList`, and replace with `List`. ### `IGListSectionType` removed In order to make building section controllers even easier, we removed the protocol and absorbed all of the methods into `IGListSectionController` with default implementations. - `numberOfItems` returns 1 item - `didUpdateToObject:` and `didSelectItemAtIndex:` do nothing - `sizeForItemAtIndex:` returns `CGSizeZero` - `cellForItemAtIndex:` asserts (you must override this method) In Objective-C, all you need to do is find & remove all uses of `IGListSectionType`. This includes `IGListSectionController` and `IGListAdapterDataSource` implementations. In Swift, you will also need to add `override` keywords to all methods. The compiler should catch all instances that need fixed. ### `IGListBindingSectionController` If you were using `IGListDiff(...)` _inside_ a section controller to compute diffs for cells, we recommend that you start using `IGListBindingSectionController` which wraps this behavior in an elegant and tested API. ### Removed `IGListCollectionView` You can simply find regex `IGListCollectionView([ |\*|\(])` and replace with regex `UICollectionView$1` in your project to fix this. ![Replace IGListCollectionView](https://raw.githubusercontent.com/Instagram/IGListKit/main/Resources/replace-iglistcollectionview.png) ### Removed `IGListGridCollectionViewLayout` Start using `IGListCollectionViewLayout` instead of `IGListGridCollectionViewLayout`. - `scrollDirection` is not yet supported. If you need horizontal scrolling, please use `UICollectionViewFlowLayout` or file an issue. - Set `minimumLineSpacing` on your [section controllers](https://github.com/Instagram/IGListKit/blob/main/Source/IGListSectionController.h#L59-L64) instead of the layout - Set `minimumInteritemSpacing` on your [section controllers](https://github.com/Instagram/IGListKit/blob/main/Source/IGListSectionController.h#L66-L71) instead of the layout - Return the size of your cells in [sizeForItemAtIndex:](https://github.com/Instagram/IGListKit/blob/main/Source/IGListSectionController.h#L48) instead of setting it on the layout. ### Item mutations must be wrapped in `-[IGListCollectionContext performBatchAnimated:completion:]` To fix some rare crashes, all item mutations must now be performed inside a batch block and done on the `IGListBatchContext` object instead. **Objective-C** ```objc // OLD self.expanded = YES; [self.collectionContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:]]; // NEW [self.collectionContext performBatchAnimated:YES updates:^(id batchContext) { self.expanded = YES; [batchContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:1]]; } completion:nil]; ``` **Swift** ```swift // OLD expanded = true collectionContext?.insert(in: self, at: [0]) // NEW collectionContext?.performBatch(animated: true, updates: { (batchContext) in self.expanded = true batchContext.insert(in: self, at: [0]) }) ``` Make sure that your model changes occur **inside the update block**, alongside the context methods. ## From 1.x to 2.x For details on all changes in `IGListKit` 2.0.0, please see the [release notes](https://github.com/Instagram/IGListKit/releases/tag/2.0.0). ### `IGListDiffable` Conformance If you relied on the default `NSObject` category, you will need to add `IGListDiffable` conformance to each of your models. To get things working as they did in 1.0, simply add the following to each of your models: **Objective-C** ```objc #import // Header @interface MyModel // Implementation - (id)diffIdentifier { return self; } - (BOOL)isEqualToDiffableObject:(id)object { return [self isEqual:object]; } ``` **Swift** ```swift import IGListKit extension MyModel: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return self } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return isEqual(object) } } ``` However we recommend writing more thorough identity and equality checks. Check out our guide to [IGListDiffable and Equality](https://instagram.github.io/IGListKit/iglistdiffable-and-equality.html) for more info. ================================================ FILE: Guides/Modeling and Binding.md ================================================ # Modeling and Binding This guide will walk you through a practical example of taking an app spec/design and turning it into a working `IGListKit` project. You will learn how to: - Turn a design spec into a top-level model and view models - Use `ListBindingSectionController` for animated, one-way cell updates - Cell-to-controller action handling and delegation - Updating the UI with local data mutations ## Getting Started You can follow along and build the example in this guide. First, you must download [this starter project](https://github.com/rnystrom/IGListKit-Binding-Guide). Open **ModelingAndBinding-Starter/ModelingAndBinding.xcworkspace** since the base project is setup with CocoaPods with `IGListKit` already added as a dependency. Take a look at the following Instagram-inspired list element design: ![Design Specs](https://raw.githubusercontent.com/Instagram/IGListKit/main/Resources/modeling-design.png) You can already start mentally modelling your data: - The top cell has a **username** and **timestamp** label - The image cell will need some sort of image `URL` - An action cell with **like count**. There will also need to be some sort of action handling when someone taps the heart - Then there are a _dynamic_ number of comment cells that contain a **username** and **comment** Remember that `IGListKit` functions on **one model per section controller**. All of the cells in this design correlate to one top-level "post" object delivered by a server. You want to create a `Post` model that contains all of the information that the cells require. > A common mistake is to create a single model and section controller for a single cell. In this example, that will create a **very confusing** architecture since the top-level objects will contain a mix and match of user, image, action, and comment models. ## Creating Models Create a new file named **Post.swift** in the starter project: ```swift import IGListKit final class Post: ListDiffable { // 1 let username: String let timestamp: String let imageURL: URL let likes: Int let comments: [Comment] // 2 init(username: String, timestamp: String, imageURL: URL, likes: Int, comments: [Comment]) { self.username = username self.timestamp = timestamp self.imageURL = imageURL self.likes = likes self.comments = comments } } ``` 1. It's best practice to always declare your values as `let` so they cannot be mutated again. The compiler will complain about the `Comment` model, ignore that for now. 2. Since `IGListKit` is compatible with Objective-C, your models must be `class`es which means writing initializers. It's only a little copy & paste! Now add a `ListDiffable` implementation inside of `Post`: ```swift // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { // 1 return (username + timestamp) as NSObjectProtocol } // 2 func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return true } ``` 1. Derive a **unique identifier** for each post. Since a single post should never have the same `username` and `timestamp` combo, we can start with that. 2. A **core requirement** to using `ListBindingSectionController` is that if two models have the same `diffIdentifier`, they **must be equal** so that the section controller can then compare view models. ### View Models Create a new Swift file named **Comment.swift** and try writing the `Comment` model yourself: - `username` of type `String` - `text` of type `String` - You will be diffing this model eventually, so add a `ListDiffable` implementation If you get stuck, or just want to copy & paste, you can reveal the implementation below.
Comment implementation

```swift import IGListKit final class Comment: ListDiffable { let username: String let text: String init(username: String, text: String) { self.username = username self.text = text } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return (username + text) as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return true } } ``` A note on the `isEqual(toDiffableObject:)` implementation: Whatever you use to derive the `diffIdentifier` can be omitted from any equality checks, since by definition the objects have already matched on their identifiers. In this case, the `username` and `text` **must be equal** by the time two objects are checked for equality.

Using the `Comment` array on a `Post` should make some sense: there are a dynamic number of comments on each post. For each comment, you want to display a cell. What might be a little bit of a new concept, though, is that you need to create models for the `UserCell`, `ImageCell`, _and_ `ActionCell` as well when working with `ListBindingSectionController`. > A binding section controller is almost like a mini-`IGListKit`. It takes an array of view models and turns them into configured cells. Get into the habit of creating a new model for each cell type within an `ListBindingSectionController` instance. With that in mind, let's start with the model for the `UserCell`: Create a new Swift file called **UserViewModel.swift**: ```swift import IGListKit final class UserViewModel: ListDiffable { let username: String let timestamp: String init(username: String, timestamp: String) { self.username = username self.timestamp = timestamp } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { // 1 return "user" as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { // 2 guard let object = object as? UserViewModel else { return false } return username == object.username && timestamp == object.timestamp } } ``` 1. Since there will only be **one `UserViewModel` per `Post`**, you can hardcode an identifier. This will enforce only a single model & cell being used. 2. It's important to write a good equality method for these view models. Anytime something changes, forcing the models to not be equal, the cell will be refreshed. Try to make view models for the **image** and **action** cell. Remember there is only a single cell per `Post`, so you can use `UserViewModel` as a starting point for how the models should look.
View model implementations

```swift import IGListKit final class ImageViewModel: ListDiffable { let url: URL init(url: URL) { self.url = url } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return "image" as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? ImageViewModel else { return false } return url == object.url } } final class ActionViewModel: ListDiffable { let likes: Int init(likes: Int) { self.likes = likes } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return "action" as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? ActionViewModel else { return false } return likes == object.likes } } ``` > You could try getting away with using generics since these models look so similar, but we've found that using **simple** models makes long-term maintenance more manageable.

## Using ListBindingSectionController You now have the following view models, which can all be derived from each `Post`: - `UserViewModel` - `ImageViewModel` - `ActionViewModel` - `Comment` Let's start using these models to power cells using `ListBindingSectionController`. This controller takes a top-level model (`Post`), asks its data source for an array of diffable view models (our view models above), then binds those view models to cells (provided in the starter project). ![Binding Flow](https://raw.githubusercontent.com/Instagram/IGListKit/main/Resources/binding-flow.png) Create **PostSectionController.swift** and add the following code: ```swift final class PostSectionController: ListBindingSectionController, ListBindingSectionControllerDataSource { override init() { super.init() dataSource = self } } ``` Notice that you are subclassing `ListBindingSectionController`. This declares your section controller as receiving a `Post` model. That way you don't have to do any special casting of your model. There are 3 methods that are required to satisfy the data source protocol: - Return an array of view models given the top-level model (`Post`) - Return a size for a given view model - Return a cell for a given view model First take care of the `Post`-to-view-models transformation: ```swift // MARK: ListBindingSectionControllerDataSource func sectionController( _ sectionController: ListBindingSectionController, viewModelsFor object: Any ) -> [ListDiffable] { // 1 guard let object = object as? Post else { fatalError() } // 2 let results: [ListDiffable] = [ UserViewModel(username: object.username, timestamp: object.timestamp), ImageViewModel(url: object.imageURL), ActionViewModel(likes: object.likes) ] // 3 return results + object.comments } ``` 1. The `object` property **must** be optional because it will not exist upon section controller initialization. However, it should never be nil at this point, nor should the `object: Any` parameter be anything but the section controller type. This is a limitation of Objective-C generics and protocols, so doing a `fatalError()` here is appropriate. 2. Create your array of view models by _decomposing_ the `Post` model into smaller models. 3. You can even append dynamic models that are delivered from the server. Next add the required API to return a size for each view model: ```swift func sectionController( _ sectionController: ListBindingSectionController, sizeForViewModel viewModel: Any, at index: Int ) -> CGSize { // 1 guard let width = collectionContext?.containerSize.width else { fatalError() } // 2 let height: CGFloat switch viewModel { case is ImageViewModel: height = 250 case is Comment: height = 35 // 3 default: height = 55 } return CGSize(width: width, height: height) } ``` 1. Just like the `object` property, the `collectionContext` should never be `nil`, but it's a weakly referenced object so must be declared as optional. Again, use `fatalError()` to catch any critical failures. 2. Swift makes checking for types so easy! Just `switch` on the type and assign a height. In Objective-C you should use `isKindOfClass:`. 3. Both the `UserViewModel` and `ActionViewModel` share the same height of `55`pts according to the design. Lastly, implement the API that returns a cell for each view model. This should look similar to the size API above. Give it a try yourself. > Remember that the cells are defined in **Main.storyboard**. You can click on each cell to view their identifiers.
"cellForViewModel:" implementation

```swift func sectionController( _ sectionController: ListBindingSectionController, cellForViewModel viewModel: Any, at index: Int ) -> UICollectionViewCell { let identifier: String switch viewModel { case is ImageViewModel: identifier = "image" case is Comment: identifier = "comment" case is UserViewModel: identifier = "user" default: identifier = "action" } guard let cell = collectionContext? .dequeueReusableCellFromStoryboard(withIdentifier: identifier, for: self, at: index) else { fatalError() } return cell } ``` Remember to handle `UserViewModel` and `ActionViewModel` separately!

## Binding Models to Cells Now you have `PostSectionController` setup to create view models, sizes, and cells. The last piece to using `ListBindingSectionController` is having your cells to receive its assigned view model and configure itself. This is done by making your cells conform to `ListBindable`. With that, `ListBindingSectionController` will **automatically** bind view models to each cell! Open **ImageCell.swift** and change the implementation to look like the following: ```swift import UIKit import SDWebImage // 1 import IGListKit // 2 final class ImageCell: UICollectionViewCell, ListBindable { @IBOutlet weak var imageView: UIImageView! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { // 3 guard let viewModel = viewModel as? ImageViewModel else { return } // 4 imageView.sd_setImage(with: viewModel.url) } } ``` 1. Make sure to import `IGListKit`! 2. Have the cell conform to `ListBindable` 3. Guard against the view model type. This will always be what `PostSectionController` pairs the cell with in `cellForViewModel:`, but guard to be safe. 4. Use the [SDWebImage](https://github.com/rs/SDWebImage) library to set the image URL. Now do exactly the same thing for each of the other cells: - `ActionCell` binds `ActionViewModel` - `UserCell` binds `UserViewModel` - `CommentCell` binds `Comment`
ListBindable implementations

```swift final class ActionCell: UICollectionViewCell, ListBindable { @IBOutlet weak var likesLabel: UILabel! @IBOutlet weak var likeButton: UIButton! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? ActionViewModel else { return } likesLabel.text = "\(viewModel.likes)" } } final class UserCell: UICollectionViewCell, ListBindable { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? UserViewModel else { return } usernameLabel.text = viewModel.username dateLabel.text = viewModel.timestamp } } final class CommentCell: UICollectionViewCell, ListBindable { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var commentLabel: UILabel! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? Comment else { return } usernameLabel.text = viewModel.username commentLabel.text = viewModel.text } } ```

## Displaying in the View Controller The very last step is getting the `PostSectionController` displaying in the app's list. Go back to **ViewController.swift** and add the following to `viewDidLoad()`, **before** setting the `dataSource` or `collectionView`: ```swift data.append(Post( username: "@janedoe", timestamp: "15min", imageURL: URL(string: "https://placekitten.com/g/375/250")!, likes: 384, comments: [ Comment(username: "@ryan", text: "this is beautiful!"), Comment(username: "@jsq", text: "😱"), Comment(username: "@caitlin", text: "#blessed"), ] )) ``` Lastly, update `listAdapter(_, sectionControllerFor object:)`: ```swift func listAdapter( _ listAdapter: ListAdapter, sectionControllerFor object: Any ) -> ListSectionController { return PostSectionController() } ``` > Normally you'd want to check the type of `object`, but since you're only using `Post` at this point, it's safe to simply return a new `PostSectionController`. **Build and run** the sample app to see your post show up! ![Working in the Simulator](https://raw.githubusercontent.com/Instagram/IGListKit/main/Resources/modeling-working.png) ## Handling Cell Actions This design should respond to tapping the heart icon on the `ActionCell`. In order to do that, you need to handle taps on the `UIButton`, then forward the event to the `PostSectionController`: Open **ActionCell.swift** and add the following protocol: ```swift protocol ActionCellDelegate: AnyObject { func didTapHeart(cell: ActionCell) } ``` Add a new delegate variable to the `ActionCell`, beneath the outlets: ```swift weak var delegate: ActionCellDelegate? = nil ``` Override `awakeFromNib()` and add a target & action to the `likeButton`: ```swift override func awakeFromNib() { super.awakeFromNib() likeButton.addTarget(self, action: #selector(ActionCell.onHeart), for: .touchUpInside) } ``` The last thing you need to do in **ActionCell.swift** is add an implementation for `onHeart()`: ```swift func onHeart() { delegate?.didTapHeart(cell: self) } ``` This will forward the button tap outside of the cell and to the delegate. Open **PostSectionController.swift** and update the `cellForViewModel:` method. Add the following at the end of the method, just after the `guard` and right before you return the `cell`: ```swift if let cell = cell as? ActionCell { cell.delegate = self } ``` The compiler will immediately complain. Satisfy the compiler but adding an empty implementation to `PostSectionController`: ```swift final class PostSectionController: ListBindingSectionController, ListBindingSectionControllerDataSource, ActionCellDelegate { //... // MARK: ActionCellDelegate func didTapHeart(cell: ActionCell) { print("like") } ``` **Build and run** the app and tap on the heart button. You should see "like"s printing into the console. ## Local Mutations Every time someone taps the heart button, you need to add a new like to the `Post`. However, all of your models are declared with `let` because immutable models are a much safer design. But if everything is immutable, how do we mutate the like count? The `PostSectionController` is the _perfect_ place to handle and store mutations. Open **PostSectionController.swift** and add the following variable: ```swift var localLikes: Int? = nil ``` Go back to the `didTapHeart(cell:)` delegate method and change the implementation to the following: ```swift func didTapHeart(cell: ActionCell) { // 1 localLikes = (localLikes ?? object?.likes ?? 0) + 1 // 2 update(animated: true) } ``` 1. Mutate the `localLikes` variable using either the previous `localLikes` or starting with `object.likes`, whichever exists. Fallback to `0` which will never happen, just satisfying the compiler. 2. Call the `update(animated:,completion:)` API on `ListBindingSectionController` to refresh the cells on the screen. In order to actually send the mutations to the models, you need to start using `localLikes` with the `ActionViewModel` which is given to the `ActionCell`. Still in **PostSectionController.swift**, find the `cellForViewModel:` API and change the `ActionViewModel` initialization to the following: ```swift ActionViewModel(likes: localLikes ?? object.likes) ``` **Build and run** the app, tap on the heart button, and see your likes increment!

## Wrapping up If you got stuck at all, or just want to play around with the example, you can find the finished project [here](https://github.com/rnystrom/IGListKit-Binding-Guide) in **ModelingAndBinding/ModelingAndBinding.xcworkspace**. `ListBindingSectionController` is one of the most powerful features that we've built for `IGListKit` because it further encourages you to design small, composable models, views, and controllers. You can also use the section controller to handle any interactions, as well as deal with mutations, just like a controller should! If you have suggestions for other topics you'd like to see, or want to offer a correction, please create a [new issue](https://github.com/Instagram/IGListKit/issues/new)! ================================================ FILE: Guides/VISION.md ================================================ # Vision This document serves to outline the long term goals of `IGListKit` and act as a guidance when making decisions about features and issues. ## Prioritizing Features & Fixes `IGListKit` is a data-driven, list-building framework built, owned, and maintained by the engineering team at Instagram. Because `IGListKit` powers parts of the Instagram iOS app, we prioritize features and bugs towards those that affect Instagram. However the team recognizes the wide range of use-cases for `IGListKit` and wants to serve as broad an audience as possible without sacrificing our own needs. ## Goals & Scope The core goal of `IGListKit` is to build fast, stable, and data-driven lists in iOS applications. That scope includes things like: - `UICollectionView` and `UITableView` integrations - Data and state management - Diffing algorithms While `IGListKit` uses specific tools, we do want to limit the reach of how we use those tools. We highly encourage people to explore solutions that fit their needs and will try to assist when possible. Examples of things beyond the scope of `IGListKit`: - Advanced/custom `UICollectionViewLayout`s - Sizing and layout (e.g. auto layout, estimated sizes) - Render and display pipelines - Integration with third-parties ## Collaboration & Community While `IGListKit` is an Instagram project, we want to give as much ownership and responsibility to the community as possible. We welcome everyone to become a collaborator on the project with whatever level of contribution you feel comfortable with. We recognize that maintaining open source projects can be demanding, and often done in addition to other responsibilities. We have no expectation for the amount or frequency of contribution from anyone. We also ask that you help keep our community welcoming and open. ## Communication GitHub Issues serve as the "source of truth" for all communication and decision-making about `IGListKit`. This keeps everything open and centralized. We will consider other forms of communication (Slack, Facebook Group, etc) once the scale of the project and/or community demands it. ================================================ FILE: Guides/Working with Core Data.md ================================================ # Working with Core Data This guide provides details on how to work with [Core Data](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/index.html) and `IGListKit`. ## Background The main difference in the setup and architecture of a Core Data and `IGListKit` application is the configuration of the model layer. Core Data operates with a mutable model layer, where objects are always passed by reference and the same instance is modified when an object is edited. `IGListKit` requires an immutable model in order to correctly calculate the diffing between model snapshots and to correctly animate the `UICollectionView`. In order to satisfy these prerequisites, Core Data `NSManagedObject`s should not be used directly as `ListDiffable` objects. Instead, a view model (or some sort of token object) should be used to mimic (or act as a placeholder for) the data that will be displayed in the collection view. ## Further discussion There are further discussions on this topic at [#460](https://github.com/Instagram/IGListKit/issues/460), [#461](https://github.com/Instagram/IGListKit/issues/461), [#407](https://github.com/Instagram/IGListKit/issues/407). ## Basic Setup The basic setup for Core Data and `IGListKit` is the same as the normal setup that is found in the [Getting Started Guide](https://instagram.github.io/IGListKit/getting-started.html). The main difference will be in the setup of the model used in the `IGListAdapterDataSource`. ## Working with view model ### Creating a view model Suppose the Core Data model consist of: ```swift extension User { @NSManaged var firstName: String @NSManaged var lastName: String @NSManaged var address: String @NSManaged var someVariableNotNeededInUI: String } ``` A `ViewModel` object will contain only the necessary information needed to build UI. The properties of the `ViewModel` will be immutable: ```swift class UserViewModel: NSObject { let firstName: String let lastName: String let address: String } ``` We recommend writing a helper method to translate Core Data objects into `ViewModel` objects: ```swift extension UserViewModel { static func fromCoreData(user: User) -> UserViewModel { // - Note: For avoiding Core Data threading violation, the following code should be wrapped in a // user.managedObjectContext?.performAndWait {} return UserViewModel(firstName: user.firstName, lastName: user.lastName, address: user.lastName) } } ``` The `IGListDiffable` protocol is implemented on the `ViewModel` layer: ```swift extension UserViewModel: ListDiffable { public func diffIdentifier() -> NSObjectProtocol { return NSString(string: firstName + lastName) } public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let toObject = object as? UserViewModel else { return false } return self.firstName == toObject.firstName && self.lastName == toObject.lastName && self.address == toObject.address } } ``` ## Setting up the view model in the adapter data source Steps to configure the `UICollectionView` with the `ViewModel`: - Retrieve Core Data objects - Transform Core Data objects into ViewModel objects and return them - Track changes to Core Data objects and update the datasource with them ### Retrieve Core Data objects The way objects are retrieved from Core Data is depends on the project. Example: Suppose there is a delegate `Provider` class with the role of fetching Core Data objects and checking for updates. It can use an `NSFetchedResultsController` to leverage on the Core Data framework and rely on automatic notifications for updates. ```swift final class UserProvider: NSObject { private lazy var userFetchResultController: NSFetchedResultsController = { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "User") // sort descriptors and predicates // ... let fetchResultController = NSFetchedResultsController( fetchRequest: tripsFetchRequest, managedObjectContext: self.coreDataStack.mainQueueManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil) // Set delegate to track CoreData changes fetchResultController.delegate = self return fetchResultController }() init(coreDataStack: CoreDataStack) { self.coreDataStack = coreDataStack super.init() do { try userFetchResultController.performFetch() } catch { fatalError("Cannot Fetch! \(error)") } } } ``` ### Transform Core Data objects into view models ```swift func getUsers() -> [UserViewModel]? { guard let users = self.userFetchResultController.fetchedObjects else { return nil } // Here we transform and return ViewModel objects! return users.flatMap { UserViewModel.fromCoreData(user: $0) } } ``` ### Track changes to Core Data The `Provider` will track changes to the Core Data model by listening to the `NSFetchedResultsController` methods and inform the application about this changes via KVO, notifications, delegation, etc. ```swift extension UserProvider: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { self.delegate?.performUpdatesForCoreDataChange(animated: true) } } ``` ### Configure the datasource The data source retrieves ViewModels and configures the `IGListSectionController` with them: ```swift func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return self.userProvider.getUsers() } ``` ### Reacting to Core Data changes in UI The `UIViewController` containing the `UICollectionView`, will react to the `NSFetchedResultController` messages by updating the UI: ```swift func performUpdatesForCoreDataChange(animated: Bool) { // Updating contents of collection view self.adapter.performUpdates(animated: animated) } ``` ================================================ FILE: Guides/Working with UICollectionView.md ================================================ # Working with `UICollectionView` This guide provides details on how to work with [`UICollectionView`](https://developer.apple.com/reference/uikit/uicollectionview) and `IGListKit`. ## Background Early versions of `IGListKit` (2.x and prior) shipped with a subclass of `UICollectionView` called [`IGListCollectionView`](https://github.com/Instagram/IGListKit/blob/2.1.0/Source/IGListCollectionView.h). The class contained *no* special functionality and was merely used to enforce compile-time restrictions to prevent users from calling certain methods directly on `UICollectionView`. Beginning with 3.0, `IGListCollectionView` [was removed](https://github.com/Instagram/IGListKit/commit/2284ce389708f62d99f48ff2ec15644f1ec59537) for a number of reasons. For further discussion see [#240](https://github.com/Instagram/IGListKit/issues/240) and [#409](https://github.com/Instagram/IGListKit/issues/409). ## Methods to avoid One of the primary purposes of `IGListKit` is to perform optimal batch updates for `UICollectionView`. Thus, clients **should never** call any APIs on `UICollectionView` that involved reloading, inserting, deleting, or otherwise updating cells and index paths. Instead, use the APIs provided by [`IGListAdapter`](https://instagram.github.io/IGListKit/Classes/IGListAdapter.html). You should also avoid setting the [`delegate`](https://developer.apple.com/reference/uikit/uicollectionview/1618033-delegate) and [`dataSource`](https://developer.apple.com/reference/uikit/uicollectionview/1618091-datasource) of the collection view, as this is also the responsibility of `IGListAdapter`. Avoid calling the following methods: ```objc - (void)performBatchUpdates:(void (^)(void))updates completion:(void (^)(BOOL))completion; - (void)reloadData; - (void)reloadSections:(NSIndexSet *)sections; - (void)insertSections:(NSIndexSet *)sections; - (void)deleteSections:(NSIndexSet *)sections; - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection; - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths; - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath; - (void)setDelegate:(id)delegate; - (void)setDataSource:(id)dataSource; - (void)setBackgroundView:(UIView *)backgroundView; ``` ## Performance In iOS 10, a new [cell prefetching API](https://developer.apple.com/reference/uikit/uicollectionviewdatasourceprefetching) was introduced. At Instagram, enabling this feature substantially degraded scrolling performance. We recommend setting [`isPrefetchingEnabled`](https://developer.apple.com/reference/uikit/uicollectionview/1771771-isprefetchingenabled) to `NO` (`false` in Swift). Note that the default value is `true`. You can set this globally using `UIAppearance`: ```objc if ([[UICollectionView class] instancesRespondToSelector:@selector(setPrefetchingEnabled:)]) { [[UICollectionView appearance] setPrefetchingEnabled:NO]; } ``` ```swift if #available(iOS 10, *) { UICollectionView.appearance().isPrefetchingEnabled = false } ``` ================================================ FILE: IGListDiffKit.podspec ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. Pod::Spec.new do |s| s.name = 'IGListDiffKit' s.version = `scripts/version.sh` s.summary = 'Diffing utilities for a data-driven UICollectionView framework.' s.homepage = 'https://github.com/Instagram/IGListKit' s.documentation_url = 'https://instagram.github.io/IGListKit' s.description = 'Diffing utilities for a data-driven UICollectionView framework for building fast and flexible lists.' s.license = { :type => 'MIT' } s.authors = 'Instagram' s.social_media_url = 'https://twitter.com/fbOpenSource' s.source = { :git => 'https://github.com/Instagram/IGListKit.git', :tag => s.version.to_s, :branch => 'stable' } s.source_files = 'Source/IGListDiffKit/**/*.{h,m,mm}' s.private_header_files = 'Source/IGListDiffKit/Internal/*.h' s.requires_arc = true s.ios.deployment_target = '11.0' s.tvos.deployment_target = '11.0' s.osx.deployment_target = '10.13' s.ios.frameworks = 'UIKit' s.tvos.frameworks = 'UIKit' s.osx.frameworks = 'Cocoa' s.library = 'c++' s.pod_target_xcconfig = { 'OTHER_CFLAGS' => '-fmodules', 'OTHER_CPLUSPLUSFLAGS' => '-fcxx-modules', 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', 'CLANG_CXX_LIBRARY' => 'libc++', } end ================================================ FILE: IGListKit.podspec ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. Pod::Spec.new do |s| s.name = 'IGListKit' s.version = `scripts/version.sh` s.summary = 'A data-driven UICollectionView framework.' s.homepage = 'https://github.com/Instagram/IGListKit' s.documentation_url = 'https://instagram.github.io/IGListKit' s.description = 'A data-driven UICollectionView framework for building fast and flexible lists.' s.license = { :type => 'MIT' } s.authors = 'Instagram' s.social_media_url = 'https://twitter.com/fbOpenSource' s.source = { :git => 'https://github.com/Instagram/IGListKit.git', :tag => s.version.to_s, :branch => 'stable' } s.dependency 'IGListDiffKit', "= #{s.version}" [s.ios, s.tvos].each do |os| os.source_files = [ 'Source/IGListDiffKit/Internal/*.h', 'Source/IGListKit/**/*.{h,m,mm}', ] os.private_header_files = [ 'Source/IGListDiffKit/Internal/*.h', 'Source/IGListKit/Internal/*.h', ] end s.osx.source_files = 'Source/IGListKit/IGListKit.h' s.requires_arc = true s.ios.deployment_target = '11.0' s.tvos.deployment_target = '11.0' s.osx.deployment_target = '10.13' s.ios.frameworks = 'UIKit' s.tvos.frameworks = 'UIKit' s.osx.frameworks = 'Cocoa' s.library = 'c++' s.pod_target_xcconfig = { 'OTHER_CFLAGS' => '-fmodules', 'OTHER_CPLUSPLUSFLAGS' => '-fcxx-modules', 'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11', 'CLANG_CXX_LIBRARY' => 'libc++', } end ================================================ FILE: IGListKit.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 0A8928F926CDA521003FABD8 /* IGListUpdateTransactionBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E722502AAC30055DC2F /* IGListUpdateTransactionBuilder.m */; }; 0A8928FA26CDA53B003FABD8 /* IGListUpdateTransactionBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7D2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h */; }; 0A8928FB26CDA591003FABD8 /* IGListReloadTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E762502AAC30055DC2F /* IGListReloadTransaction.h */; }; 0A8928FC26CDA5BD003FABD8 /* IGListReloadTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E7E2502AAC40055DC2F /* IGListReloadTransaction.m */; }; 0A8928FD26CDA5E1003FABD8 /* IGListDataSourceChangeTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E792502AAC30055DC2F /* IGListDataSourceChangeTransaction.m */; }; 0A8928FE26CDA5EA003FABD8 /* IGListDataSourceChangeTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E742502AAC30055DC2F /* IGListDataSourceChangeTransaction.h */; }; 0A8928FF26CDA62C003FABD8 /* IGListBatchUpdateTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */; }; 0A89290026CDA632003FABD8 /* IGListBatchUpdateTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7B2502AAC40055DC2F /* IGListBatchUpdateTransaction.h */; }; 0A89290126CDA666003FABD8 /* IGListItemUpdatesCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7C2502AAC40055DC2F /* IGListItemUpdatesCollector.h */; }; 0A89290226CDA672003FABD8 /* IGListItemUpdatesCollector.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E752502AAC30055DC2F /* IGListItemUpdatesCollector.m */; }; 0B3B93611E08E38C008390ED /* IGListBatchUpdateDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE51D870EDC007C7F66 /* IGListBatchUpdateDataTests.m */; }; 13DF01731FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01721FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m */; }; 13DF01741FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01721FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m */; }; 13DF01771FA1000E0092A320 /* IGTestReorderableSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01761FA1000E0092A320 /* IGTestReorderableSection.m */; }; 13DF01781FA1000E0092A320 /* IGTestReorderableSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF01761FA1000E0092A320 /* IGTestReorderableSection.m */; }; 16B71CEB22B0A08400FE96ED /* IGTestInvalidateLayoutSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CE722B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.m */; }; 16B71CEC22B0A08400FE96ED /* IGTestInvalidateLayoutSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CE722B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.m */; }; 16B71CED22B0A08400FE96ED /* IGTestInvalidateLayoutDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CE822B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.m */; }; 16B71CEE22B0A08400FE96ED /* IGTestInvalidateLayoutDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CE822B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.m */; }; 16B71CEF22B0A08400FE96ED /* IGTestInvalidateLayoutObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CEA22B0A08300FE96ED /* IGTestInvalidateLayoutObject.m */; }; 16B71CF022B0A08400FE96ED /* IGTestInvalidateLayoutObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 16B71CEA22B0A08300FE96ED /* IGTestInvalidateLayoutObject.m */; }; 22907ABD2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907ABC2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m */; }; 22907ABE2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907ABC2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m */; }; 22907AC12F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC02F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m */; }; 22907AC22F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC02F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m */; }; 22907AC42F2866160015F3D0 /* IGListUpdateCoalescerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC32F2866160015F3D0 /* IGListUpdateCoalescerTests.m */; }; 22907AC52F2866160015F3D0 /* IGListUpdateCoalescerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC32F2866160015F3D0 /* IGListUpdateCoalescerTests.m */; }; 22907AC72F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC62F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m */; }; 22907AC82F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC62F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m */; }; 22907ACA2F2870020015F3D0 /* IGListPerformDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC92F2870020015F3D0 /* IGListPerformDiffTests.m */; }; 22907ACB2F2870020015F3D0 /* IGListPerformDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 22907AC92F2870020015F3D0 /* IGListPerformDiffTests.m */; }; 26271C8A1DAE94E40073E116 /* IGTestSingleNibItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 26271C891DAE94E40073E116 /* IGTestSingleNibItemDataSource.m */; }; 26271C8C1DAE96740073E116 /* IGListSingleNibItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 26271C8B1DAE96740073E116 /* IGListSingleNibItemControllerTests.m */; }; 290DF3771E9323E6009FE456 /* IGListDebuggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 290DF3761E9323E6009FE456 /* IGListDebuggerTests.m */; }; 2914BEE91DCD15F400C96401 /* IGTestNibSupplementaryView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2904861C1DCD02140007F41D /* IGTestNibSupplementaryView.xib */; }; 294AC6321DDE4C19002FCE5D /* IGListDiffResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */; }; 298DD9CE1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9CD1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m */; }; 298DD9CF1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9CD1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m */; }; 298DDA001E3AE28000F76F50 /* IGTestDiffingObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9F91E3AE1AA00F76F50 /* IGTestDiffingObject.m */; }; 298DDA011E3AE28000F76F50 /* IGTestDiffingObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9F91E3AE1AA00F76F50 /* IGTestDiffingObject.m */; }; 298DDA051E3AE2B000F76F50 /* IGTestStringBindableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9D91E3ADE3300F76F50 /* IGTestStringBindableCell.m */; }; 298DDA061E3AE2B000F76F50 /* IGTestNumberBindableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9E11E3ADE4300F76F50 /* IGTestNumberBindableCell.m */; }; 298DDA071E3AE2B100F76F50 /* IGTestStringBindableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9D91E3ADE3300F76F50 /* IGTestStringBindableCell.m */; }; 298DDA081E3AE2B100F76F50 /* IGTestNumberBindableCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9E11E3ADE4300F76F50 /* IGTestNumberBindableCell.m */; }; 298DDA091E3AE31D00F76F50 /* IGTestDiffingSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9D11E3ADDB400F76F50 /* IGTestDiffingSectionController.m */; }; 298DDA0A1E3AE31E00F76F50 /* IGTestDiffingSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DD9D11E3ADDB400F76F50 /* IGTestDiffingSectionController.m */; }; 298DDA131E3AE3F100F76F50 /* IGTestDiffingDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA0C1E3AE3ED00F76F50 /* IGTestDiffingDataSource.m */; }; 298DDA141E3AE3F300F76F50 /* IGTestDiffingDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA0C1E3AE3ED00F76F50 /* IGTestDiffingDataSource.m */; }; 298DDA241E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA231E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m */; }; 298DDA251E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA231E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m */; }; 298DDA381E3B168E00F76F50 /* IGLayoutTestItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA291E3B166100F76F50 /* IGLayoutTestItem.m */; }; 298DDA391E3B168F00F76F50 /* IGLayoutTestItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA291E3B166100F76F50 /* IGLayoutTestItem.m */; }; 298DDA3A1E3B16F600F76F50 /* IGLayoutTestDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA271E3B166100F76F50 /* IGLayoutTestDataSource.m */; }; 298DDA3B1E3B16F800F76F50 /* IGLayoutTestDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA271E3B166100F76F50 /* IGLayoutTestDataSource.m */; }; 298DDA3C1E3B170300F76F50 /* IGLayoutTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA2B1E3B166100F76F50 /* IGLayoutTestSection.m */; }; 298DDA3D1E3B170400F76F50 /* IGLayoutTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 298DDA2B1E3B166100F76F50 /* IGLayoutTestSection.m */; }; 2995409E1F588C9400F647CF /* IGTestBindingWithoutDeselectionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2995409B1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.m */; }; 2995409F1F588C9500F647CF /* IGTestBindingWithoutDeselectionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2995409B1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.m */; }; 29C4748C1DDF45F400AE68CE /* IGListAdapterProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29C4748A1DDF45E700AE68CE /* IGListAdapterProxyTests.m */; }; 29C4748D1DDF45F900AE68CE /* IGListAdapterProxyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29C4748A1DDF45E700AE68CE /* IGListAdapterProxyTests.m */; }; 29C4748E1DDF460500AE68CE /* IGListAdapterStoryboardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F11DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m */; }; 29C4748F1DDF460500AE68CE /* IGListDiffResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */; }; 29C474901DDF460500AE68CE /* IGListSectionMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 829D7BA81DD1816400549816 /* IGListSectionMapTests.m */; }; 29C5792E1DE0DA89003A149B /* IGTestNibSupplementaryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2904861F1DCD02750007F41D /* IGTestNibSupplementaryView.m */; }; 29C5792F1DE0DA8A003A149B /* IGListTestAdapterStoryboardDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */; }; 29C579301DE0DA8A003A149B /* IGListTestStoryboardSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F71DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m */; }; 29C579311DE0DA8A003A149B /* IGTestNibSupplementaryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2904861F1DCD02750007F41D /* IGTestNibSupplementaryView.m */; }; 29C579321DE0DA8A003A149B /* IGTestStoryboardSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F41DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m */; }; 29C579331DE0DA8A003A149B /* IGTestStoryboardSupplementaryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7EF1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m */; }; 29DA5CA31EA7C72400113926 /* IGListGenericSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */; }; 29DA5CA71EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; 29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA61EA7D37000113926 /* IGListTestCase.m */; }; 29EA6C491DB43A8000957A88 /* IGTestNibCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */; }; 576029DC2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */; }; 576029DD2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */; }; 576029DE2C61B91D006E50E2 /* IGListPerformDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D62C61B91D006E50E2 /* IGListPerformDiff.h */; }; 576029DF2C61B91D006E50E2 /* IGListPerformDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D62C61B91D006E50E2 /* IGListPerformDiff.h */; }; 576029E02C61B91D006E50E2 /* IGListPerformDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029D72C61B91D006E50E2 /* IGListPerformDiff.m */; }; 576029E12C61B91D006E50E2 /* IGListPerformDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029D72C61B91D006E50E2 /* IGListPerformDiff.m */; }; 576029E22C61B91D006E50E2 /* IGListUpdateCoalescer.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D82C61B91D006E50E2 /* IGListUpdateCoalescer.h */; }; 576029E32C61B91D006E50E2 /* IGListUpdateCoalescer.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029D82C61B91D006E50E2 /* IGListUpdateCoalescer.h */; }; 576029E42C61B91D006E50E2 /* IGListViewVisibilityTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029D92C61B91D006E50E2 /* IGListViewVisibilityTracker.m */; }; 576029E52C61B91D006E50E2 /* IGListViewVisibilityTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029D92C61B91D006E50E2 /* IGListViewVisibilityTracker.m */; }; 576029E62C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */; }; 576029E72C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */; }; 576029E82C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */; }; 576029E92C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */ = {isa = PBXBuildFile; fileRef = 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */; }; 5766613E2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */; }; 5766613F2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */; }; 57B22E6C2502AAB20055DC2F /* IGListTransitionData.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E662502AAB10055DC2F /* IGListTransitionData.m */; }; 57B22E6F2502AAB20055DC2F /* IGListTransitionData.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E692502AAB10055DC2F /* IGListTransitionData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 57B22E7F2502AAC40055DC2F /* IGListBatchUpdateTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */; }; 57B22E802502AAC40055DC2F /* IGListUpdateTransactionBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E722502AAC30055DC2F /* IGListUpdateTransactionBuilder.m */; }; 57B22E812502AAC40055DC2F /* IGListUpdateTransactable.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E732502AAC30055DC2F /* IGListUpdateTransactable.h */; }; 57B22E822502AAC40055DC2F /* IGListDataSourceChangeTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E742502AAC30055DC2F /* IGListDataSourceChangeTransaction.h */; }; 57B22E832502AAC40055DC2F /* IGListItemUpdatesCollector.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E752502AAC30055DC2F /* IGListItemUpdatesCollector.m */; }; 57B22E842502AAC40055DC2F /* IGListReloadTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E762502AAC30055DC2F /* IGListReloadTransaction.h */; }; 57B22E872502AAC40055DC2F /* IGListDataSourceChangeTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E792502AAC30055DC2F /* IGListDataSourceChangeTransaction.m */; }; 57B22E892502AAC40055DC2F /* IGListBatchUpdateTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7B2502AAC40055DC2F /* IGListBatchUpdateTransaction.h */; }; 57B22E8A2502AAC40055DC2F /* IGListItemUpdatesCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7C2502AAC40055DC2F /* IGListItemUpdatesCollector.h */; }; 57B22E8B2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E7D2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h */; }; 57B22E8C2502AAC40055DC2F /* IGListReloadTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E7E2502AAC40055DC2F /* IGListReloadTransaction.m */; }; 6A9EB3611F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A9EB3601F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m */; }; 6A9EB3621F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A9EB3601F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m */; }; 7A02CEEE2361511100B49FAE /* IGListReloadDataUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC72361510F00B49FAE /* IGListReloadDataUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEEF2361511100B49FAE /* IGListReloadDataUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC72361510F00B49FAE /* IGListReloadDataUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF12361511100B49FAE /* IGListScrollDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC82361510F00B49FAE /* IGListScrollDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF22361511100B49FAE /* IGListScrollDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC82361510F00B49FAE /* IGListScrollDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF42361511100B49FAE /* IGListWorkingRangeDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC92361510F00B49FAE /* IGListWorkingRangeDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF52361511100B49FAE /* IGListWorkingRangeDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEC92361510F00B49FAE /* IGListWorkingRangeDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF72361511100B49FAE /* IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECA2361510F00B49FAE /* IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEF82361511100B49FAE /* IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECA2361510F00B49FAE /* IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEFA2361511100B49FAE /* IGListDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECB2361510F00B49FAE /* IGListDisplayDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEFB2361511100B49FAE /* IGListDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECB2361510F00B49FAE /* IGListDisplayDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEFD2361511100B49FAE /* IGListCollectionViewDelegateLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECC2361510F00B49FAE /* IGListCollectionViewDelegateLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CEFE2361511100B49FAE /* IGListCollectionViewDelegateLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECC2361510F00B49FAE /* IGListCollectionViewDelegateLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF002361511100B49FAE /* IGListCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECD2361510F00B49FAE /* IGListCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF012361511100B49FAE /* IGListCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECD2361510F00B49FAE /* IGListCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF032361511100B49FAE /* IGListGenericSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECE2361510F00B49FAE /* IGListGenericSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF042361511100B49FAE /* IGListGenericSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECE2361510F00B49FAE /* IGListGenericSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF062361511100B49FAE /* IGListBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECF2361510F00B49FAE /* IGListBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF072361511100B49FAE /* IGListBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CECF2361510F00B49FAE /* IGListBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF092361511100B49FAE /* IGListBindingSectionControllerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED02361510F00B49FAE /* IGListBindingSectionControllerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF0A2361511100B49FAE /* IGListBindingSectionControllerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED02361510F00B49FAE /* IGListBindingSectionControllerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF0C2361511100B49FAE /* IGListCollectionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED12361510F00B49FAE /* IGListCollectionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF0D2361511100B49FAE /* IGListCollectionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED12361510F00B49FAE /* IGListCollectionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF0F2361511100B49FAE /* IGListAdapterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED22361511000B49FAE /* IGListAdapterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF102361511100B49FAE /* IGListAdapterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED22361511000B49FAE /* IGListAdapterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF122361511100B49FAE /* IGListGenericSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CED32361511000B49FAE /* IGListGenericSectionController.m */; }; 7A02CF132361511100B49FAE /* IGListGenericSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CED32361511000B49FAE /* IGListGenericSectionController.m */; }; 7A02CF152361511100B49FAE /* IGListCollectionViewLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED42361511000B49FAE /* IGListCollectionViewLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF162361511100B49FAE /* IGListCollectionViewLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED42361511000B49FAE /* IGListCollectionViewLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF182361511100B49FAE /* IGListAdapterMoveDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED52361511000B49FAE /* IGListAdapterMoveDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF192361511100B49FAE /* IGListAdapterMoveDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED52361511000B49FAE /* IGListAdapterMoveDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF1B2361511100B49FAE /* IGListSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED62361511000B49FAE /* IGListSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF1C2361511100B49FAE /* IGListSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED62361511000B49FAE /* IGListSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF1E2361511100B49FAE /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED72361511000B49FAE /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF1F2361511100B49FAE /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED72361511000B49FAE /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF212361511100B49FAE /* IGListTransitionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED82361511000B49FAE /* IGListTransitionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF222361511100B49FAE /* IGListTransitionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED82361511000B49FAE /* IGListTransitionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF242361511100B49FAE /* IGListAdapterUpdateListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED92361511000B49FAE /* IGListAdapterUpdateListener.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF252361511100B49FAE /* IGListAdapterUpdateListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED92361511000B49FAE /* IGListAdapterUpdateListener.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF272361511100B49FAE /* IGListBindable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDA2361511000B49FAE /* IGListBindable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF282361511100B49FAE /* IGListBindable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDA2361511000B49FAE /* IGListBindable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF2A2361511100B49FAE /* IGListReloadDataUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEDB2361511000B49FAE /* IGListReloadDataUpdater.m */; }; 7A02CF2B2361511100B49FAE /* IGListReloadDataUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEDB2361511000B49FAE /* IGListReloadDataUpdater.m */; }; 7A02CF2D2361511100B49FAE /* IGListBindingSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDC2361511000B49FAE /* IGListBindingSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF2E2361511100B49FAE /* IGListBindingSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDC2361511000B49FAE /* IGListBindingSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF302361511100B49FAE /* IGListUpdatingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDD2361511000B49FAE /* IGListUpdatingDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF312361511100B49FAE /* IGListUpdatingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDD2361511000B49FAE /* IGListUpdatingDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF332361511100B49FAE /* IGListAdapterUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEDE2361511000B49FAE /* IGListAdapterUpdater.m */; }; 7A02CF342361511100B49FAE /* IGListAdapterUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEDE2361511000B49FAE /* IGListAdapterUpdater.m */; }; 7A02CF362361511100B49FAE /* IGListAdapterDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDF2361511000B49FAE /* IGListAdapterDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF372361511100B49FAE /* IGListAdapterDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEDF2361511000B49FAE /* IGListAdapterDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF392361511100B49FAE /* IGListCollectionViewLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE02361511000B49FAE /* IGListCollectionViewLayout.mm */; }; 7A02CF3A2361511100B49FAE /* IGListCollectionViewLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE02361511000B49FAE /* IGListCollectionViewLayout.mm */; }; 7A02CF3C2361511100B49FAE /* IGListCollectionViewLayoutCompatible.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE12361511000B49FAE /* IGListCollectionViewLayoutCompatible.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF3D2361511100B49FAE /* IGListCollectionViewLayoutCompatible.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE12361511000B49FAE /* IGListCollectionViewLayoutCompatible.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF3F2361511100B49FAE /* IGListAdapterUpdaterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE22361511000B49FAE /* IGListAdapterUpdaterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF402361511100B49FAE /* IGListAdapterUpdaterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE22361511000B49FAE /* IGListAdapterUpdaterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF422361511100B49FAE /* IGListSingleSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE32361511000B49FAE /* IGListSingleSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF432361511100B49FAE /* IGListSingleSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE32361511000B49FAE /* IGListSingleSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF452361511100B49FAE /* IGListAdapterPerformanceDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE42361511000B49FAE /* IGListAdapterPerformanceDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF462361511100B49FAE /* IGListAdapterPerformanceDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE42361511000B49FAE /* IGListAdapterPerformanceDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF482361511100B49FAE /* IGListSingleSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE52361511000B49FAE /* IGListSingleSectionController.m */; }; 7A02CF492361511100B49FAE /* IGListSingleSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE52361511000B49FAE /* IGListSingleSectionController.m */; }; 7A02CF4B2361511100B49FAE /* IGListSupplementaryViewSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE62361511000B49FAE /* IGListSupplementaryViewSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF4C2361511100B49FAE /* IGListSupplementaryViewSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE62361511000B49FAE /* IGListSupplementaryViewSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF4E2361511100B49FAE /* IGListCollectionScrollingTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE72361511000B49FAE /* IGListCollectionScrollingTraits.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF4F2361511100B49FAE /* IGListCollectionScrollingTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE72361511000B49FAE /* IGListCollectionScrollingTraits.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF512361511100B49FAE /* IGListAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE82361511000B49FAE /* IGListAdapter.m */; }; 7A02CF522361511100B49FAE /* IGListAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEE82361511000B49FAE /* IGListAdapter.m */; }; 7A02CF542361511100B49FAE /* IGListBindingSectionControllerSelectionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE92361511000B49FAE /* IGListBindingSectionControllerSelectionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF552361511100B49FAE /* IGListBindingSectionControllerSelectionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEE92361511000B49FAE /* IGListBindingSectionControllerSelectionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF572361511100B49FAE /* IGListBindingSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEEA2361511100B49FAE /* IGListBindingSectionController.m */; }; 7A02CF582361511100B49FAE /* IGListBindingSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEEA2361511100B49FAE /* IGListBindingSectionController.m */; }; 7A02CF5A2361511100B49FAE /* IGListAdapterUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEEB2361511100B49FAE /* IGListAdapterUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF5B2361511100B49FAE /* IGListAdapterUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CEEB2361511100B49FAE /* IGListAdapterUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02CF5D2361511100B49FAE /* IGListSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEEC2361511100B49FAE /* IGListSectionController.m */; }; 7A02CF5E2361511100B49FAE /* IGListSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEEC2361511100B49FAE /* IGListSectionController.m */; }; 7A02CF602361511100B49FAE /* IGListCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEED2361511100B49FAE /* IGListCollectionView.m */; }; 7A02CF612361511100B49FAE /* IGListCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CEED2361511100B49FAE /* IGListCollectionView.m */; }; 7A02CF902361513600B49FAE /* IGListDisplayHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF642361513300B49FAE /* IGListDisplayHandler.h */; }; 7A02CF912361513600B49FAE /* IGListDisplayHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF642361513300B49FAE /* IGListDisplayHandler.h */; }; 7A02CF932361513600B49FAE /* IGListAdapter+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF652361513300B49FAE /* IGListAdapter+DebugDescription.m */; }; 7A02CF942361513600B49FAE /* IGListAdapter+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF652361513300B49FAE /* IGListAdapter+DebugDescription.m */; }; 7A02CF962361513600B49FAE /* IGListAdapterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF662361513400B49FAE /* IGListAdapterInternal.h */; }; 7A02CF972361513600B49FAE /* IGListAdapterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF662361513400B49FAE /* IGListAdapterInternal.h */; }; 7A02CF992361513600B49FAE /* IGListBindingSectionController+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF672361513400B49FAE /* IGListBindingSectionController+DebugDescription.h */; }; 7A02CF9A2361513600B49FAE /* IGListBindingSectionController+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF672361513400B49FAE /* IGListBindingSectionController+DebugDescription.h */; }; 7A02CF9C2361513600B49FAE /* IGListCollectionViewLayoutInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF682361513400B49FAE /* IGListCollectionViewLayoutInternal.h */; }; 7A02CF9D2361513600B49FAE /* IGListCollectionViewLayoutInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF682361513400B49FAE /* IGListCollectionViewLayoutInternal.h */; }; 7A02CFA22361513600B49FAE /* UIScrollView+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF6A2361513400B49FAE /* UIScrollView+IGListKit.h */; }; 7A02CFA32361513600B49FAE /* UIScrollView+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF6A2361513400B49FAE /* UIScrollView+IGListKit.h */; }; 7A02CFA52361513600B49FAE /* UICollectionView+IGListBatchUpdateData.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6B2361513400B49FAE /* UICollectionView+IGListBatchUpdateData.m */; }; 7A02CFA62361513600B49FAE /* UICollectionView+IGListBatchUpdateData.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6B2361513400B49FAE /* UICollectionView+IGListBatchUpdateData.m */; }; 7A02CFA82361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF6C2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.h */; }; 7A02CFA92361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF6C2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.h */; }; 7A02CFAB2361513600B49FAE /* IGListReloadIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6D2361513400B49FAE /* IGListReloadIndexPath.m */; }; 7A02CFAC2361513600B49FAE /* IGListReloadIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6D2361513400B49FAE /* IGListReloadIndexPath.m */; }; 7A02CFAE2361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6E2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.m */; }; 7A02CFAF2361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6E2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.m */; }; 7A02CFB12361513600B49FAE /* UIScrollView+IGListKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6F2361513400B49FAE /* UIScrollView+IGListKit.m */; }; 7A02CFB22361513600B49FAE /* UIScrollView+IGListKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF6F2361513400B49FAE /* UIScrollView+IGListKit.m */; }; 7A02CFB42361513600B49FAE /* IGListAdapterUpdaterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF702361513400B49FAE /* IGListAdapterUpdaterInternal.h */; }; 7A02CFB52361513600B49FAE /* IGListAdapterUpdaterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF702361513400B49FAE /* IGListAdapterUpdaterInternal.h */; }; 7A02CFB72361513600B49FAE /* IGListSectionMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF712361513400B49FAE /* IGListSectionMap.h */; }; 7A02CFB82361513600B49FAE /* IGListSectionMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF712361513400B49FAE /* IGListSectionMap.h */; }; 7A02CFBA2361513600B49FAE /* IGListAdapterProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF722361513400B49FAE /* IGListAdapterProxy.m */; }; 7A02CFBB2361513600B49FAE /* IGListAdapterProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF722361513400B49FAE /* IGListAdapterProxy.m */; }; 7A02CFBD2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF732361513400B49FAE /* IGListAdapterUpdater+DebugDescription.m */; }; 7A02CFBE2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF732361513400B49FAE /* IGListAdapterUpdater+DebugDescription.m */; }; 7A02CFC02361513600B49FAE /* IGListAdapter+UICollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF742361513400B49FAE /* IGListAdapter+UICollectionView.h */; }; 7A02CFC12361513600B49FAE /* IGListAdapter+UICollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF742361513400B49FAE /* IGListAdapter+UICollectionView.h */; }; 7A02CFC32361513600B49FAE /* UICollectionView+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF752361513400B49FAE /* UICollectionView+DebugDescription.m */; }; 7A02CFC42361513600B49FAE /* UICollectionView+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF752361513400B49FAE /* UICollectionView+DebugDescription.m */; }; 7A02CFC62361513600B49FAE /* IGListSectionMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF762361513400B49FAE /* IGListSectionMap.m */; }; 7A02CFC72361513600B49FAE /* IGListSectionMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF762361513400B49FAE /* IGListSectionMap.m */; }; 7A02CFC92361513600B49FAE /* UICollectionView+IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF772361513400B49FAE /* UICollectionView+IGListBatchUpdateData.h */; }; 7A02CFCA2361513600B49FAE /* UICollectionView+IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF772361513400B49FAE /* UICollectionView+IGListBatchUpdateData.h */; }; 7A02CFCC2361513600B49FAE /* IGListBatchUpdateState.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF782361513400B49FAE /* IGListBatchUpdateState.h */; }; 7A02CFCD2361513600B49FAE /* IGListBatchUpdateState.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF782361513400B49FAE /* IGListBatchUpdateState.h */; }; 7A02CFCF2361513600B49FAE /* IGListDebugger.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF792361513400B49FAE /* IGListDebugger.h */; }; 7A02CFD02361513600B49FAE /* IGListDebugger.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF792361513400B49FAE /* IGListDebugger.h */; }; 7A02CFD82361513600B49FAE /* IGListBatchUpdateData+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF7C2361513400B49FAE /* IGListBatchUpdateData+DebugDescription.m */; }; 7A02CFD92361513600B49FAE /* IGListBatchUpdateData+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF7C2361513400B49FAE /* IGListBatchUpdateData+DebugDescription.m */; }; 7A02CFDB2361513600B49FAE /* IGListAdapterProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7D2361513400B49FAE /* IGListAdapterProxy.h */; }; 7A02CFDC2361513600B49FAE /* IGListAdapterProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7D2361513400B49FAE /* IGListAdapterProxy.h */; }; 7A02CFDE2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7E2361513500B49FAE /* IGListAdapterUpdater+DebugDescription.h */; }; 7A02CFDF2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7E2361513500B49FAE /* IGListAdapterUpdater+DebugDescription.h */; }; 7A02CFE12361513600B49FAE /* IGListAdapter+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7F2361513500B49FAE /* IGListAdapter+DebugDescription.h */; }; 7A02CFE22361513600B49FAE /* IGListAdapter+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF7F2361513500B49FAE /* IGListAdapter+DebugDescription.h */; }; 7A02CFE42361513600B49FAE /* IGListDisplayHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF802361513500B49FAE /* IGListDisplayHandler.m */; }; 7A02CFE52361513600B49FAE /* IGListDisplayHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF802361513500B49FAE /* IGListDisplayHandler.m */; }; 7A02CFE72361513600B49FAE /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF812361513500B49FAE /* IGListArrayUtilsInternal.h */; }; 7A02CFE82361513600B49FAE /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF812361513500B49FAE /* IGListArrayUtilsInternal.h */; }; 7A02CFED2361513600B49FAE /* IGListDebuggingUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF832361513500B49FAE /* IGListDebuggingUtilities.h */; }; 7A02CFEE2361513600B49FAE /* IGListDebuggingUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF832361513500B49FAE /* IGListDebuggingUtilities.h */; }; 7A02CFF02361513600B49FAE /* IGListBindingSectionController+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF842361513500B49FAE /* IGListBindingSectionController+DebugDescription.m */; }; 7A02CFF12361513600B49FAE /* IGListBindingSectionController+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF842361513500B49FAE /* IGListBindingSectionController+DebugDescription.m */; }; 7A02CFF32361513600B49FAE /* IGListAdapter+UICollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF852361513500B49FAE /* IGListAdapter+UICollectionView.m */; }; 7A02CFF42361513600B49FAE /* IGListAdapter+UICollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF852361513500B49FAE /* IGListAdapter+UICollectionView.m */; }; 7A02CFF62361513600B49FAE /* IGListSectionMap+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF862361513500B49FAE /* IGListSectionMap+DebugDescription.h */; }; 7A02CFF72361513600B49FAE /* IGListSectionMap+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF862361513500B49FAE /* IGListSectionMap+DebugDescription.h */; }; 7A02CFF92361513600B49FAE /* IGListWorkingRangeHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF872361513500B49FAE /* IGListWorkingRangeHandler.mm */; }; 7A02CFFA2361513600B49FAE /* IGListWorkingRangeHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF872361513500B49FAE /* IGListWorkingRangeHandler.mm */; }; 7A02CFFC2361513600B49FAE /* IGListReloadIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF882361513500B49FAE /* IGListReloadIndexPath.h */; }; 7A02CFFD2361513600B49FAE /* IGListReloadIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF882361513500B49FAE /* IGListReloadIndexPath.h */; }; 7A02CFFF2361513600B49FAE /* IGListDebugger.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF892361513500B49FAE /* IGListDebugger.m */; }; 7A02D0002361513600B49FAE /* IGListDebugger.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF892361513500B49FAE /* IGListDebugger.m */; }; 7A02D0022361513600B49FAE /* IGListSectionControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8A2361513500B49FAE /* IGListSectionControllerInternal.h */; }; 7A02D0032361513600B49FAE /* IGListSectionControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8A2361513500B49FAE /* IGListSectionControllerInternal.h */; }; 7A02D0052361513600B49FAE /* IGListDebuggingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF8B2361513500B49FAE /* IGListDebuggingUtilities.m */; }; 7A02D0062361513600B49FAE /* IGListDebuggingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF8B2361513500B49FAE /* IGListDebuggingUtilities.m */; }; 7A02D0082361513600B49FAE /* IGListBatchUpdateData+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8C2361513500B49FAE /* IGListBatchUpdateData+DebugDescription.h */; }; 7A02D0092361513600B49FAE /* IGListBatchUpdateData+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8C2361513500B49FAE /* IGListBatchUpdateData+DebugDescription.h */; }; 7A02D00B2361513600B49FAE /* IGListSectionMap+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF8D2361513600B49FAE /* IGListSectionMap+DebugDescription.m */; }; 7A02D00C2361513600B49FAE /* IGListSectionMap+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02CF8D2361513600B49FAE /* IGListSectionMap+DebugDescription.m */; }; 7A02D00E2361513600B49FAE /* IGListWorkingRangeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8E2361513600B49FAE /* IGListWorkingRangeHandler.h */; }; 7A02D00F2361513600B49FAE /* IGListWorkingRangeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8E2361513600B49FAE /* IGListWorkingRangeHandler.h */; }; 7A02D0112361513600B49FAE /* UICollectionView+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8F2361513600B49FAE /* UICollectionView+DebugDescription.h */; }; 7A02D0122361513600B49FAE /* UICollectionView+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CF8F2361513600B49FAE /* UICollectionView+DebugDescription.h */; }; 7A02D05B2361529F00B49FAE /* IGListDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0402361529E00B49FAE /* IGListDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D05C2361529F00B49FAE /* IGListDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0402361529E00B49FAE /* IGListDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D05D2361529F00B49FAE /* IGListDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0402361529E00B49FAE /* IGListDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D05E2361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0412361529E00B49FAE /* NSNumber+IGListDiffable.m */; }; 7A02D05F2361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0412361529E00B49FAE /* NSNumber+IGListDiffable.m */; }; 7A02D0602361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0412361529E00B49FAE /* NSNumber+IGListDiffable.m */; }; 7A02D0612361529F00B49FAE /* NSString+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0422361529E00B49FAE /* NSString+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0622361529F00B49FAE /* NSString+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0422361529E00B49FAE /* NSString+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0632361529F00B49FAE /* NSString+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0422361529E00B49FAE /* NSString+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0642361529F00B49FAE /* IGListIndexPathResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0432361529E00B49FAE /* IGListIndexPathResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0652361529F00B49FAE /* IGListIndexPathResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0432361529E00B49FAE /* IGListIndexPathResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0662361529F00B49FAE /* IGListIndexPathResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0432361529E00B49FAE /* IGListIndexPathResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0672361529F00B49FAE /* IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0442361529E00B49FAE /* IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0682361529F00B49FAE /* IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0442361529E00B49FAE /* IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0692361529F00B49FAE /* IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0442361529E00B49FAE /* IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D06A2361529F00B49FAE /* IGListExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0452361529E00B49FAE /* IGListExperiments.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D06B2361529F00B49FAE /* IGListExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0452361529E00B49FAE /* IGListExperiments.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D06C2361529F00B49FAE /* IGListExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0452361529E00B49FAE /* IGListExperiments.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D06D2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0462361529E00B49FAE /* NSString+IGListDiffable.m */; }; 7A02D06E2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0462361529E00B49FAE /* NSString+IGListDiffable.m */; }; 7A02D06F2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0462361529E00B49FAE /* NSString+IGListDiffable.m */; }; 7A02D0702361529F00B49FAE /* IGListCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0472361529E00B49FAE /* IGListCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0712361529F00B49FAE /* IGListCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0472361529E00B49FAE /* IGListCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0722361529F00B49FAE /* IGListCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0472361529E00B49FAE /* IGListCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0732361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0482361529E00B49FAE /* NSNumber+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0742361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0482361529E00B49FAE /* NSNumber+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0752361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0482361529E00B49FAE /* NSNumber+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0762361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04A2361529E00B49FAE /* IGListIndexSetResultInternal.h */; }; 7A02D0772361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04A2361529E00B49FAE /* IGListIndexSetResultInternal.h */; }; 7A02D0782361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04A2361529E00B49FAE /* IGListIndexSetResultInternal.h */; }; 7A02D0792361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04B2361529E00B49FAE /* IGListIndexPathResultInternal.h */; }; 7A02D07A2361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04B2361529E00B49FAE /* IGListIndexPathResultInternal.h */; }; 7A02D07B2361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04B2361529E00B49FAE /* IGListIndexPathResultInternal.h */; }; 7A02D07C2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04C2361529E00B49FAE /* IGListMoveIndexInternal.h */; }; 7A02D07D2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04C2361529E00B49FAE /* IGListMoveIndexInternal.h */; }; 7A02D07E2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04C2361529E00B49FAE /* IGListMoveIndexInternal.h */; }; 7A02D07F2361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04D2361529E00B49FAE /* IGListMoveIndexPathInternal.h */; }; 7A02D0802361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04D2361529E00B49FAE /* IGListMoveIndexPathInternal.h */; }; 7A02D0812361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04D2361529E00B49FAE /* IGListMoveIndexPathInternal.h */; }; 7A02D0822361529F00B49FAE /* IGListMoveIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04E2361529E00B49FAE /* IGListMoveIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0832361529F00B49FAE /* IGListMoveIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04E2361529E00B49FAE /* IGListMoveIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0842361529F00B49FAE /* IGListMoveIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D04E2361529E00B49FAE /* IGListMoveIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0852361529F00B49FAE /* IGListDiff.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D04F2361529E00B49FAE /* IGListDiff.mm */; }; 7A02D0862361529F00B49FAE /* IGListDiff.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D04F2361529E00B49FAE /* IGListDiff.mm */; }; 7A02D0872361529F00B49FAE /* IGListDiff.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D04F2361529E00B49FAE /* IGListDiff.mm */; }; 7A02D0882361529F00B49FAE /* IGListAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0502361529E00B49FAE /* IGListAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0892361529F00B49FAE /* IGListAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0502361529E00B49FAE /* IGListAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D08A2361529F00B49FAE /* IGListAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0502361529E00B49FAE /* IGListAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D08B2361529F00B49FAE /* IGListIndexPathResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0512361529E00B49FAE /* IGListIndexPathResult.m */; }; 7A02D08C2361529F00B49FAE /* IGListIndexPathResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0512361529E00B49FAE /* IGListIndexPathResult.m */; }; 7A02D08D2361529F00B49FAE /* IGListIndexPathResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0512361529E00B49FAE /* IGListIndexPathResult.m */; }; 7A02D08E2361529F00B49FAE /* IGListIndexSetResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0522361529E00B49FAE /* IGListIndexSetResult.m */; }; 7A02D08F2361529F00B49FAE /* IGListIndexSetResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0522361529E00B49FAE /* IGListIndexSetResult.m */; }; 7A02D0902361529F00B49FAE /* IGListIndexSetResult.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0522361529E00B49FAE /* IGListIndexSetResult.m */; }; 7A02D0912361529F00B49FAE /* IGListMoveIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0532361529E00B49FAE /* IGListMoveIndex.m */; }; 7A02D0922361529F00B49FAE /* IGListMoveIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0532361529E00B49FAE /* IGListMoveIndex.m */; }; 7A02D0932361529F00B49FAE /* IGListMoveIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0532361529E00B49FAE /* IGListMoveIndex.m */; }; 7A02D0942361529F00B49FAE /* IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0542361529E00B49FAE /* IGListBatchUpdateData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0952361529F00B49FAE /* IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0542361529E00B49FAE /* IGListBatchUpdateData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0962361529F00B49FAE /* IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0542361529E00B49FAE /* IGListBatchUpdateData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0972361529F00B49FAE /* IGListMoveIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0552361529E00B49FAE /* IGListMoveIndexPath.m */; }; 7A02D0982361529F00B49FAE /* IGListMoveIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0552361529E00B49FAE /* IGListMoveIndexPath.m */; }; 7A02D0992361529F00B49FAE /* IGListMoveIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0552361529E00B49FAE /* IGListMoveIndexPath.m */; }; 7A02D09A2361529F00B49FAE /* IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0562361529E00B49FAE /* IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D09B2361529F00B49FAE /* IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0562361529E00B49FAE /* IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D09C2361529F00B49FAE /* IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0562361529E00B49FAE /* IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D09D2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0572361529E00B49FAE /* IGListBatchUpdateData.mm */; }; 7A02D09E2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0572361529E00B49FAE /* IGListBatchUpdateData.mm */; }; 7A02D09F2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A02D0572361529E00B49FAE /* IGListBatchUpdateData.mm */; }; 7A02D0A02361529F00B49FAE /* IGListMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0582361529E00B49FAE /* IGListMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A12361529F00B49FAE /* IGListMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0582361529E00B49FAE /* IGListMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A22361529F00B49FAE /* IGListMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0582361529E00B49FAE /* IGListMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A32361529F00B49FAE /* IGListMoveIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0592361529E00B49FAE /* IGListMoveIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A42361529F00B49FAE /* IGListMoveIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0592361529E00B49FAE /* IGListMoveIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A52361529F00B49FAE /* IGListMoveIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D0592361529E00B49FAE /* IGListMoveIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A62361529F00B49FAE /* IGListIndexSetResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D05A2361529F00B49FAE /* IGListIndexSetResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A72361529F00B49FAE /* IGListIndexSetResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D05A2361529F00B49FAE /* IGListIndexSetResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A82361529F00B49FAE /* IGListIndexSetResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02D05A2361529F00B49FAE /* IGListIndexSetResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A02D0A9236152F500B49FAE /* IGListDiffKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A02D0382361526600B49FAE /* IGListDiffKit.framework */; }; 7A02D0AA2361530800B49FAE /* IGListDiffKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A02D02B2361525800B49FAE /* IGListDiffKit.framework */; }; 7A02D0AB2361530E00B49FAE /* IGListDiffKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A02D01D2361520200B49FAE /* IGListDiffKit.framework */; }; 7A02D0C023615CE500B49FAE /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A02CED72361511000B49FAE /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A92EA9B23A8A42000E5A13D /* IGListSwiftKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A92EA9923A8A42000E5A13D /* IGListSwiftKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A92EAB023A8A50C00E5A13D /* IGListSwiftKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A92EA9923A8A42000E5A13D /* IGListSwiftKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A92EAB423A8A5FA00E5A13D /* IGListAdapterUpdaterHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AD6864A23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h */; }; 7A92EAB523A8A5FF00E5A13D /* IGListAdapterUpdaterHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD6864923A89E7F009000DE /* IGListAdapterUpdaterHelpers.m */; }; 7AD6864B23A89E7F009000DE /* IGListAdapterUpdaterHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD6864923A89E7F009000DE /* IGListAdapterUpdaterHelpers.m */; }; 7AD6864C23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AD6864A23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h */; }; 821BC4C01DB8C9D500172ED0 /* IGListSingleStoryboardItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */; }; 821BC4C41DB8CEF800172ED0 /* IGTestStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */; }; 821BC4CB1DB8D60100172ED0 /* IGTestStoryboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4C81DB8D5B200172ED0 /* IGTestStoryboardViewController.m */; }; 821BC4D01DB8D90900172ED0 /* IGTestStoryboardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4CD1DB8D8C500172ED0 /* IGTestStoryboardCell.m */; }; 821BC4D31DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4D21DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m */; }; 8240C7F01DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7EF1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m */; }; 8240C7F21DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F11DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m */; }; 8240C7F51DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F41DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m */; }; 8240C7F81DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7F71DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m */; }; 8240C7FB1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */; }; 8285404C1DE40C6E00118B94 /* IGListTestHorizontalSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */; }; 8285404D1DE40C6E00118B94 /* IGListTestHorizontalSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */; }; 828540501DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */; }; 828540511DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */; }; 82914C5B1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */; }; 82914C5C1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */; }; 829D7BAA1DD1819000549816 /* IGListSectionMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 829D7BA81DD1816400549816 /* IGListSectionMapTests.m */; }; 88144F071D870EDC007C7F66 /* IGListAdapterE2ETests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE21D870EDC007C7F66 /* IGListAdapterE2ETests.m */; }; 88144F081D870EDC007C7F66 /* IGListAdapterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE31D870EDC007C7F66 /* IGListAdapterTests.m */; }; 88144F091D870EDC007C7F66 /* IGListAdapterUpdaterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE41D870EDC007C7F66 /* IGListAdapterUpdaterTests.m */; }; 88144F0A1D870EDC007C7F66 /* IGListBatchUpdateDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE51D870EDC007C7F66 /* IGListBatchUpdateDataTests.m */; }; 88144F0B1D870EDC007C7F66 /* IGListDiffSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */; }; 88144F0C1D870EDC007C7F66 /* IGListDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE81D870EDC007C7F66 /* IGListDiffTests.m */; }; 88144F0D1D870EDC007C7F66 /* IGListDisplayHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE91D870EDC007C7F66 /* IGListDisplayHandlerTests.m */; }; 88144F101D870EDC007C7F66 /* IGListSingleSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EED1D870EDC007C7F66 /* IGListSingleSectionControllerTests.m */; }; 88144F121D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EEF1D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m */; }; 88144F131D870EDC007C7F66 /* IGListTestAdapterDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF21D870EDC007C7F66 /* IGListTestAdapterDataSource.m */; }; 88144F141D870EDC007C7F66 /* IGListTestOffsettingLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF41D870EDC007C7F66 /* IGListTestOffsettingLayout.m */; }; 88144F151D870EDC007C7F66 /* IGListTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF61D870EDC007C7F66 /* IGListTestSection.m */; }; 88144F161D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF81D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m */; }; 88144F171D870EDC007C7F66 /* IGTestCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFA1D870EDC007C7F66 /* IGTestCell.m */; }; 88144F181D870EDC007C7F66 /* IGTestDelegateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFC1D870EDC007C7F66 /* IGTestDelegateController.m */; }; 88144F191D870EDC007C7F66 /* IGTestDelegateDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFE1D870EDC007C7F66 /* IGTestDelegateDataSource.m */; }; 88144F1A1D870EDC007C7F66 /* IGTestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F001D870EDC007C7F66 /* IGTestObject.m */; }; 88144F1B1D870EDC007C7F66 /* IGTestSingleItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F021D870EDC007C7F66 /* IGTestSingleItemDataSource.m */; }; 88144F1D1D870EDC007C7F66 /* IGTestSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */; }; 882BC1321E0118CB0083B311 /* IGTestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F001D870EDC007C7F66 /* IGTestObject.m */; }; 883797082022304E00B94676 /* (null) in Headers */ = {isa = PBXBuildFile; }; 88379728202236AB00B94676 /* IGListAdapterUpdateTester.m in Sources */ = {isa = PBXBuildFile; fileRef = 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */; }; 88379729202236AC00B94676 /* IGListAdapterUpdateTester.m in Sources */ = {isa = PBXBuildFile; fileRef = 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */; }; 885FE1DD1DC51A0D009CE2B4 /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 885FE1D41DC51A0D009CE2B4 /* IGListKit.framework */; }; 885FE22B1DC51B76009CE2B4 /* IGListAdapterE2ETests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE21D870EDC007C7F66 /* IGListAdapterE2ETests.m */; }; 885FE22C1DC51B76009CE2B4 /* IGListAdapterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE31D870EDC007C7F66 /* IGListAdapterTests.m */; }; 885FE22D1DC51B76009CE2B4 /* IGListAdapterUpdaterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE41D870EDC007C7F66 /* IGListAdapterUpdaterTests.m */; }; 885FE22E1DC51B76009CE2B4 /* IGListBatchUpdateDataTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE51D870EDC007C7F66 /* IGListBatchUpdateDataTests.m */; }; 885FE22F1DC51B76009CE2B4 /* IGListDiffSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */; }; 885FE2301DC51B76009CE2B4 /* IGListDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE81D870EDC007C7F66 /* IGListDiffTests.m */; }; 885FE2311DC51B76009CE2B4 /* IGListDisplayHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE91D870EDC007C7F66 /* IGListDisplayHandlerTests.m */; }; 885FE2331DC51B76009CE2B4 /* IGListSingleSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EED1D870EDC007C7F66 /* IGListSingleSectionControllerTests.m */; }; 885FE2341DC51B76009CE2B4 /* IGListSingleNibItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 26271C8B1DAE96740073E116 /* IGListSingleNibItemControllerTests.m */; }; 885FE2351DC51B76009CE2B4 /* IGListSingleStoryboardItemControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */; }; 885FE2371DC51B76009CE2B4 /* IGListWorkingRangeHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EEF1D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m */; }; 885FE2381DC51B86009CE2B4 /* IGListTestAdapterDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF21D870EDC007C7F66 /* IGListTestAdapterDataSource.m */; }; 885FE2391DC51B86009CE2B4 /* IGListTestOffsettingLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF41D870EDC007C7F66 /* IGListTestOffsettingLayout.m */; }; 885FE23A1DC51B86009CE2B4 /* IGListTestSection.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF61D870EDC007C7F66 /* IGListTestSection.m */; }; 885FE23B1DC51B86009CE2B4 /* IGListTestUICollectionViewDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EF81D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m */; }; 885FE23C1DC51B86009CE2B4 /* IGTestCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFA1D870EDC007C7F66 /* IGTestCell.m */; }; 885FE23D1DC51B86009CE2B4 /* IGTestDelegateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFC1D870EDC007C7F66 /* IGTestDelegateController.m */; }; 885FE23E1DC51B86009CE2B4 /* IGTestDelegateDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EFE1D870EDC007C7F66 /* IGTestDelegateDataSource.m */; }; 885FE23F1DC51B86009CE2B4 /* IGTestObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F001D870EDC007C7F66 /* IGTestObject.m */; }; 885FE2401DC51B86009CE2B4 /* IGTestSingleItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F021D870EDC007C7F66 /* IGTestSingleItemDataSource.m */; }; 885FE2411DC51B86009CE2B4 /* IGTestSingleNibItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 26271C891DAE94E40073E116 /* IGTestSingleNibItemDataSource.m */; }; 885FE2421DC51B86009CE2B4 /* IGTestSingleStoryboardItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4D21DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m */; }; 885FE2431DC51B86009CE2B4 /* IGTestStoryboardViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4C81DB8D5B200172ED0 /* IGTestStoryboardViewController.m */; }; 885FE2451DC51B86009CE2B4 /* IGTestStoryboardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 821BC4CD1DB8D8C500172ED0 /* IGTestStoryboardCell.m */; }; 885FE2461DC51B86009CE2B4 /* IGTestSupplementarySource.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */; }; 887D0B401D870D7F009E01F7 /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 887D0B361D870D7E009E01F7 /* IGListKit.framework */; }; 88DF89811E010E6A00B1B9B4 /* IGListKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD3152AC1DE227FA00AC9D2C /* IGListKit.framework */; }; 88DF89881E010F5C00B1B9B4 /* IGListDiffResultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */; }; 88DF89891E010F6500B1B9B4 /* IGListDiffSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */; }; 88DF898A1E010F7000B1B9B4 /* IGListDiffTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 88144EE81D870EDC007C7F66 /* IGListDiffTests.m */; }; A46A1D202D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D1F2D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h */; }; A46A1D212D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D1F2D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h */; }; A46A1D232D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D222D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h */; }; A46A1D242D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D222D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h */; }; A46A1D2B2D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D262D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m */; }; A46A1D2C2D80213D00CB9157 /* UIViewController+IGListAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D2A2D80213D00CB9157 /* UIViewController+IGListAdapter.m */; }; A46A1D2E2D80213D00CB9157 /* UIViewController+IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D292D80213D00CB9157 /* UIViewController+IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; A46A1D302D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D252D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h */; settings = {ATTRIBUTES = (Public, ); }; }; A46A1D312D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D262D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m */; }; A46A1D322D80213D00CB9157 /* UIViewController+IGListAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D2A2D80213D00CB9157 /* UIViewController+IGListAdapter.m */; }; A46A1D342D80213D00CB9157 /* UIViewController+IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D292D80213D00CB9157 /* UIViewController+IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; A46A1D362D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D252D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h */; settings = {ATTRIBUTES = (Public, ); }; }; A46A1D392D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D382D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m */; }; A46A1D3A2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D372D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; A46A1D3B2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m in Sources */ = {isa = PBXBuildFile; fileRef = A46A1D382D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m */; }; A46A1D3C2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h in Headers */ = {isa = PBXBuildFile; fileRef = A46A1D372D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; E03DEA83255C9AAC00ACCAFC /* IGListTransitionData.h in Headers */ = {isa = PBXBuildFile; fileRef = 57B22E692502AAB10055DC2F /* IGListTransitionData.h */; settings = {ATTRIBUTES = (Public, ); }; }; E03DEA8F255C9AB200ACCAFC /* IGListTransitionData.m in Sources */ = {isa = PBXBuildFile; fileRef = 57B22E662502AAB10055DC2F /* IGListTransitionData.m */; }; E56B7B3420A9D7100071010C /* IGListCollectionScrollingTraitsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E56B7B3320A9D7100071010C /* IGListCollectionScrollingTraitsTests.m */; }; E56B7B3520A9D7100071010C /* IGListCollectionScrollingTraitsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E56B7B3320A9D7100071010C /* IGListCollectionScrollingTraitsTests.m */; }; E8D312E01FC472A60009FA2F /* IGListContentInsetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E8D312DF1FC472A60009FA2F /* IGListContentInsetTests.m */; }; F102DBBB2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = F102DBBA2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m */; }; F102DBBC2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m in Sources */ = {isa = PBXBuildFile; fileRef = F102DBBA2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m */; }; F10C8F572B982EE6009F4690 /* IGListDefaultExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = F10C8F562B982DFD009F4690 /* IGListDefaultExperiments.h */; }; F10C8F582B982EE7009F4690 /* IGListDefaultExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = F10C8F562B982DFD009F4690 /* IGListDefaultExperiments.h */; }; F10C8F5B2B9830F0009F4690 /* IGListTestCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F10C8F592B9830E6009F4690 /* IGListTestCollectionViewLayout.m */; }; F10C8F5C2B9830F1009F4690 /* IGListTestCollectionViewLayout.m in Sources */ = {isa = PBXBuildFile; fileRef = F10C8F592B9830E6009F4690 /* IGListTestCollectionViewLayout.m */; }; F12D8EAA2E93852600A7DDBE /* OCMock in Frameworks */ = {isa = PBXBuildFile; productRef = F12D8EA92E93852600A7DDBE /* OCMock */; }; F12D8EAC2E93853500A7DDBE /* OCMock in Frameworks */ = {isa = PBXBuildFile; productRef = F12D8EAB2E93853500A7DDBE /* OCMock */; }; F12D8EAF2E93854100A7DDBE /* OCMock in Frameworks */ = {isa = PBXBuildFile; productRef = F12D8EAE2E93854100A7DDBE /* OCMock */; }; F15DB2F12D813B090066C7AD /* IGListAdapter+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EC2D813B090066C7AD /* IGListAdapter+Async.swift */; }; F15DB2F22D813B090066C7AD /* ListValueSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2F02D813B090066C7AD /* ListValueSectionController.swift */; }; F15DB2F32D813B090066C7AD /* IGListSingleSectionController+Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EE2D813B090066C7AD /* IGListSingleSectionController+Refinements.swift */; }; F15DB2F42D813B090066C7AD /* IGListCollectionContext+Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2ED2D813B090066C7AD /* IGListCollectionContext+Refinements.swift */; }; F15DB2F52D813B090066C7AD /* ListIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EF2D813B090066C7AD /* ListIdentifiable.swift */; }; F15DB2F62D813B090066C7AD /* IGListAdapter+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EC2D813B090066C7AD /* IGListAdapter+Async.swift */; }; F15DB2F72D813B090066C7AD /* ListValueSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2F02D813B090066C7AD /* ListValueSectionController.swift */; }; F15DB2F82D813B090066C7AD /* IGListSingleSectionController+Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EE2D813B090066C7AD /* IGListSingleSectionController+Refinements.swift */; }; F15DB2F92D813B090066C7AD /* IGListCollectionContext+Refinements.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2ED2D813B090066C7AD /* IGListCollectionContext+Refinements.swift */; }; F15DB2FA2D813B090066C7AD /* ListIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15DB2EF2D813B090066C7AD /* ListIdentifiable.swift */; }; F1855A4C29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1855A4B29BC565600558D18 /* IGListDiffDescriptionStringTests.m */; }; F1855A4D29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1855A4B29BC565600558D18 /* IGListDiffDescriptionStringTests.m */; }; F1855A4E29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1855A4B29BC565600558D18 /* IGListDiffDescriptionStringTests.m */; }; F18CC75C29EFBB9400DC3B9A /* IGListCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC75B29EFBB9400DC3B9A /* IGListCollectionViewTests.m */; }; F18CC75D29EFBB9400DC3B9A /* IGListCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC75B29EFBB9400DC3B9A /* IGListCollectionViewTests.m */; }; F18CC75F29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC75E29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m */; }; F18CC76029EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC75E29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m */; }; F18CC76329EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76129EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m */; }; F18CC76429EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76129EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m */; }; F18CC76629EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76529EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m */; }; F18CC76729EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76529EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m */; }; F18CC76A29EFBD0300DC3B9A /* IGListBindingSingleSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = F18CC76829EFBD0300DC3B9A /* IGListBindingSingleSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; F18CC76B29EFBD0300DC3B9A /* IGListBindingSingleSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = F18CC76829EFBD0300DC3B9A /* IGListBindingSingleSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; F18CC76C29EFBD0300DC3B9A /* IGListBindingSingleSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76929EFBD0300DC3B9A /* IGListBindingSingleSectionController.m */; }; F18CC76D29EFBD0300DC3B9A /* IGListBindingSingleSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = F18CC76929EFBD0300DC3B9A /* IGListBindingSingleSectionController.m */; }; F1E6302329EA43080060B4D6 /* IGListSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1E6302229EA43080060B4D6 /* IGListSectionControllerTests.m */; }; F1E6302429EA43080060B4D6 /* IGListSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1E6302229EA43080060B4D6 /* IGListSectionControllerTests.m */; }; F1ED68B329E9B3B9003744F8 /* IGListInteractiveMovingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68AE29E9B3B9003744F8 /* IGListInteractiveMovingTests.m */; }; F1ED68B529E9B3B9003744F8 /* IGListDebugDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68AF29E9B3B9003744F8 /* IGListDebugDescriptionTests.m */; }; F1ED68B729E9B3B9003744F8 /* IGListTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68B029E9B3B9003744F8 /* IGListTransactionTests.m */; }; F1ED68BA29E9B404003744F8 /* IGListInteractiveMovingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68AE29E9B3B9003744F8 /* IGListInteractiveMovingTests.m */; }; F1ED68BB29E9B40A003744F8 /* IGListGenericSectionControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */; }; F1ED68BC29E9B411003744F8 /* IGListDebuggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 290DF3761E9323E6009FE456 /* IGListDebuggerTests.m */; }; F1ED68BD29E9B415003744F8 /* IGListDebugDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68AF29E9B3B9003744F8 /* IGListDebugDescriptionTests.m */; }; F1ED68BE29E9B41A003744F8 /* IGListContentInsetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E8D312DF1FC472A60009FA2F /* IGListContentInsetTests.m */; }; F1ED68BF29E9B443003744F8 /* IGListTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F1ED68B029E9B3B9003744F8 /* IGListTransactionTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 7A02D0B22361546300B49FAE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 7A02D01C2361520200B49FAE; remoteInfo = IGListDiffKit; }; 7A02D0BC23615B8B00B49FAE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 7A02D02A2361525800B49FAE; remoteInfo = "IGListDiffKit-tvOS"; }; 7A02D0BE23615B9000B49FAE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 7A02D0372361526600B49FAE; remoteInfo = "IGListDiffKit-macOS"; }; 7A92EAA123A8A4E700E5A13D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 887D0B351D870D7E009E01F7; remoteInfo = IGListKit; }; 7A92EAB223A8A53F00E5A13D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 885FE1D31DC51A0D009CE2B4; remoteInfo = "IGListKit-tvOS"; }; 885FE1DE1DC51A0D009CE2B4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 885FE1D31DC51A0D009CE2B4; remoteInfo = "IGListKit-tvOS"; }; 887D0B411D870D7F009E01F7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = 887D0B351D870D7E009E01F7; remoteInfo = IGListKit; }; 88DF89821E010E6A00B1B9B4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 887D0B2D1D870D7E009E01F7 /* Project object */; proxyType = 1; remoteGlobalIDString = DD3152661DE227FA00AC9D2C; remoteInfo = "IGListKit-macOS"; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ F15F1DDF2BA40CF4002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DE12BA40D15002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DE32BA40D27002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DE52BA40D37002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DE72BA40D5E002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DE92BA40D6D002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DEB2BA40D7C002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; F15F1DED2BA40D94002AF3FE /* Copy Privacy Manifest */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 7; files = ( ); name = "Copy Privacy Manifest"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 13DF01711FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListTestAdapterReorderingDataSource.h; sourceTree = ""; }; 13DF01721FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListTestAdapterReorderingDataSource.m; sourceTree = ""; }; 13DF01751FA1000E0092A320 /* IGTestReorderableSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGTestReorderableSection.h; sourceTree = ""; }; 13DF01761FA1000E0092A320 /* IGTestReorderableSection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGTestReorderableSection.m; sourceTree = ""; }; 16B71CE522B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestInvalidateLayoutDataSource.h; sourceTree = ""; }; 16B71CE622B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestInvalidateLayoutSectionController.h; sourceTree = ""; }; 16B71CE722B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestInvalidateLayoutSectionController.m; sourceTree = ""; }; 16B71CE822B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestInvalidateLayoutDataSource.m; sourceTree = ""; }; 16B71CE922B0A08300FE96ED /* IGTestInvalidateLayoutObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestInvalidateLayoutObject.h; sourceTree = ""; }; 16B71CEA22B0A08300FE96ED /* IGTestInvalidateLayoutObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestInvalidateLayoutObject.m; sourceTree = ""; }; 22907ABC2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListViewVisibilityTrackerTests.m; sourceTree = ""; }; 22907AC02F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListItemUpdatesCollectorTests.m; sourceTree = ""; }; 22907AC32F2866160015F3D0 /* IGListUpdateCoalescerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListUpdateCoalescerTests.m; sourceTree = ""; }; 22907AC62F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIViewControllerIGListAdapterTests.m; sourceTree = ""; }; 22907AC92F2870020015F3D0 /* IGListPerformDiffTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListPerformDiffTests.m; sourceTree = ""; }; 26271C881DAE94E40073E116 /* IGTestSingleNibItemDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestSingleNibItemDataSource.h; sourceTree = ""; }; 26271C891DAE94E40073E116 /* IGTestSingleNibItemDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestSingleNibItemDataSource.m; sourceTree = ""; }; 26271C8B1DAE96740073E116 /* IGListSingleNibItemControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleNibItemControllerTests.m; sourceTree = ""; }; 2904861C1DCD02140007F41D /* IGTestNibSupplementaryView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IGTestNibSupplementaryView.xib; sourceTree = ""; }; 2904861E1DCD02750007F41D /* IGTestNibSupplementaryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestNibSupplementaryView.h; sourceTree = ""; }; 2904861F1DCD02750007F41D /* IGTestNibSupplementaryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestNibSupplementaryView.m; sourceTree = ""; }; 290DF3761E9323E6009FE456 /* IGListDebuggerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDebuggerTests.m; sourceTree = ""; }; 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IGTestNibCell.xib; sourceTree = ""; }; 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDiffResultTests.m; sourceTree = ""; }; 294CDE611F995DD7002CF6E4 /* IGListAdapterUpdateTester.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdateTester.h; sourceTree = ""; }; 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterUpdateTester.m; sourceTree = ""; }; 298DD9CD1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBindingSectionControllerTests.m; sourceTree = ""; }; 298DD9D01E3ADDB400F76F50 /* IGTestDiffingSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestDiffingSectionController.h; sourceTree = ""; }; 298DD9D11E3ADDB400F76F50 /* IGTestDiffingSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestDiffingSectionController.m; sourceTree = ""; }; 298DD9D81E3ADE3300F76F50 /* IGTestStringBindableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStringBindableCell.h; sourceTree = ""; }; 298DD9D91E3ADE3300F76F50 /* IGTestStringBindableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestStringBindableCell.m; sourceTree = ""; }; 298DD9E01E3ADE4300F76F50 /* IGTestNumberBindableCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestNumberBindableCell.h; sourceTree = ""; }; 298DD9E11E3ADE4300F76F50 /* IGTestNumberBindableCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestNumberBindableCell.m; sourceTree = ""; }; 298DD9F81E3AE1AA00F76F50 /* IGTestDiffingObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestDiffingObject.h; sourceTree = ""; }; 298DD9F91E3AE1AA00F76F50 /* IGTestDiffingObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestDiffingObject.m; sourceTree = ""; }; 298DDA0B1E3AE3ED00F76F50 /* IGTestDiffingDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestDiffingDataSource.h; sourceTree = ""; }; 298DDA0C1E3AE3ED00F76F50 /* IGTestDiffingDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestDiffingDataSource.m; sourceTree = ""; }; 298DDA231E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListCollectionViewLayoutTests.m; sourceTree = ""; }; 298DDA261E3B166100F76F50 /* IGLayoutTestDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGLayoutTestDataSource.h; sourceTree = ""; }; 298DDA271E3B166100F76F50 /* IGLayoutTestDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGLayoutTestDataSource.m; sourceTree = ""; }; 298DDA281E3B166100F76F50 /* IGLayoutTestItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGLayoutTestItem.h; sourceTree = ""; }; 298DDA291E3B166100F76F50 /* IGLayoutTestItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGLayoutTestItem.m; sourceTree = ""; }; 298DDA2A1E3B166100F76F50 /* IGLayoutTestSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGLayoutTestSection.h; sourceTree = ""; }; 298DDA2B1E3B166100F76F50 /* IGLayoutTestSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGLayoutTestSection.m; sourceTree = ""; }; 2995409A1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestBindingWithoutDeselectionDelegate.h; sourceTree = ""; }; 2995409B1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestBindingWithoutDeselectionDelegate.m; sourceTree = ""; }; 29C4748A1DDF45E700AE68CE /* IGListAdapterProxyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterProxyTests.m; sourceTree = ""; }; 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListGenericSectionControllerTests.m; sourceTree = ""; }; 29DA5CA61EA7D37000113926 /* IGListTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestCase.m; sourceTree = ""; }; 29DA5CA91EA7D39B00113926 /* IGListTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestCase.h; sourceTree = ""; }; 29DA5CAA1EA7D3FF00113926 /* IGListTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestHelpers.h; sourceTree = ""; }; 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListViewVisibilityTracker.h; sourceTree = ""; }; 576029D62C61B91D006E50E2 /* IGListPerformDiff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListPerformDiff.h; sourceTree = ""; }; 576029D72C61B91D006E50E2 /* IGListPerformDiff.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListPerformDiff.m; sourceTree = ""; }; 576029D82C61B91D006E50E2 /* IGListUpdateCoalescer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListUpdateCoalescer.h; sourceTree = ""; }; 576029D92C61B91D006E50E2 /* IGListViewVisibilityTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListViewVisibilityTracker.m; sourceTree = ""; }; 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListViewVisibilityTrackerInternal.h; sourceTree = ""; }; 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListUpdateCoalescer.m; sourceTree = ""; }; 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterDelegateAnnouncerTests.m; sourceTree = ""; }; 57B22E662502AAB10055DC2F /* IGListTransitionData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTransitionData.m; sourceTree = ""; }; 57B22E692502AAB10055DC2F /* IGListTransitionData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTransitionData.h; sourceTree = ""; }; 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBatchUpdateTransaction.m; sourceTree = ""; }; 57B22E722502AAC30055DC2F /* IGListUpdateTransactionBuilder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListUpdateTransactionBuilder.m; sourceTree = ""; }; 57B22E732502AAC30055DC2F /* IGListUpdateTransactable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListUpdateTransactable.h; sourceTree = ""; }; 57B22E742502AAC30055DC2F /* IGListDataSourceChangeTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDataSourceChangeTransaction.h; sourceTree = ""; }; 57B22E752502AAC30055DC2F /* IGListItemUpdatesCollector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListItemUpdatesCollector.m; sourceTree = ""; }; 57B22E762502AAC30055DC2F /* IGListReloadTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListReloadTransaction.h; sourceTree = ""; }; 57B22E792502AAC30055DC2F /* IGListDataSourceChangeTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDataSourceChangeTransaction.m; sourceTree = ""; }; 57B22E7B2502AAC40055DC2F /* IGListBatchUpdateTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchUpdateTransaction.h; sourceTree = ""; }; 57B22E7C2502AAC40055DC2F /* IGListItemUpdatesCollector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListItemUpdatesCollector.h; sourceTree = ""; }; 57B22E7D2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListUpdateTransactionBuilder.h; sourceTree = ""; }; 57B22E7E2502AAC40055DC2F /* IGListReloadTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListReloadTransaction.m; sourceTree = ""; }; 6A9EB35F1F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGTestSingleWithoutDeselectionDelegate.h; sourceTree = ""; }; 6A9EB3601F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGTestSingleWithoutDeselectionDelegate.m; sourceTree = ""; }; 7A02CEC72361510F00B49FAE /* IGListReloadDataUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListReloadDataUpdater.h; sourceTree = ""; }; 7A02CEC82361510F00B49FAE /* IGListScrollDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListScrollDelegate.h; sourceTree = ""; }; 7A02CEC92361510F00B49FAE /* IGListWorkingRangeDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListWorkingRangeDelegate.h; sourceTree = ""; }; 7A02CECA2361510F00B49FAE /* IGListAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapter.h; sourceTree = ""; }; 7A02CECB2361510F00B49FAE /* IGListDisplayDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDisplayDelegate.h; sourceTree = ""; }; 7A02CECC2361510F00B49FAE /* IGListCollectionViewDelegateLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionViewDelegateLayout.h; sourceTree = ""; }; 7A02CECD2361510F00B49FAE /* IGListCollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionView.h; sourceTree = ""; }; 7A02CECE2361510F00B49FAE /* IGListGenericSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListGenericSectionController.h; sourceTree = ""; }; 7A02CECF2361510F00B49FAE /* IGListBatchContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchContext.h; sourceTree = ""; }; 7A02CED02361510F00B49FAE /* IGListBindingSectionControllerDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBindingSectionControllerDataSource.h; sourceTree = ""; }; 7A02CED12361510F00B49FAE /* IGListCollectionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionContext.h; sourceTree = ""; }; 7A02CED22361511000B49FAE /* IGListAdapterDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterDelegate.h; sourceTree = ""; }; 7A02CED32361511000B49FAE /* IGListGenericSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListGenericSectionController.m; sourceTree = ""; }; 7A02CED42361511000B49FAE /* IGListCollectionViewLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionViewLayout.h; sourceTree = ""; }; 7A02CED52361511000B49FAE /* IGListAdapterMoveDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterMoveDelegate.h; sourceTree = ""; }; 7A02CED62361511000B49FAE /* IGListSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListSectionController.h; sourceTree = ""; }; 7A02CED72361511000B49FAE /* IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListKit.h; sourceTree = ""; }; 7A02CED82361511000B49FAE /* IGListTransitionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTransitionDelegate.h; sourceTree = ""; }; 7A02CED92361511000B49FAE /* IGListAdapterUpdateListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdateListener.h; sourceTree = ""; }; 7A02CEDA2361511000B49FAE /* IGListBindable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBindable.h; sourceTree = ""; }; 7A02CEDB2361511000B49FAE /* IGListReloadDataUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListReloadDataUpdater.m; sourceTree = ""; }; 7A02CEDC2361511000B49FAE /* IGListBindingSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBindingSectionController.h; sourceTree = ""; }; 7A02CEDD2361511000B49FAE /* IGListUpdatingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListUpdatingDelegate.h; sourceTree = ""; }; 7A02CEDE2361511000B49FAE /* IGListAdapterUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterUpdater.m; sourceTree = ""; }; 7A02CEDF2361511000B49FAE /* IGListAdapterDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterDataSource.h; sourceTree = ""; }; 7A02CEE02361511000B49FAE /* IGListCollectionViewLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = IGListCollectionViewLayout.mm; sourceTree = ""; }; 7A02CEE12361511000B49FAE /* IGListCollectionViewLayoutCompatible.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionViewLayoutCompatible.h; sourceTree = ""; }; 7A02CEE22361511000B49FAE /* IGListAdapterUpdaterDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdaterDelegate.h; sourceTree = ""; }; 7A02CEE32361511000B49FAE /* IGListSingleSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListSingleSectionController.h; sourceTree = ""; }; 7A02CEE42361511000B49FAE /* IGListAdapterPerformanceDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterPerformanceDelegate.h; sourceTree = ""; }; 7A02CEE52361511000B49FAE /* IGListSingleSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleSectionController.m; sourceTree = ""; }; 7A02CEE62361511000B49FAE /* IGListSupplementaryViewSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListSupplementaryViewSource.h; sourceTree = ""; }; 7A02CEE72361511000B49FAE /* IGListCollectionScrollingTraits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionScrollingTraits.h; sourceTree = ""; }; 7A02CEE82361511000B49FAE /* IGListAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapter.m; sourceTree = ""; }; 7A02CEE92361511000B49FAE /* IGListBindingSectionControllerSelectionDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBindingSectionControllerSelectionDelegate.h; sourceTree = ""; }; 7A02CEEA2361511100B49FAE /* IGListBindingSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBindingSectionController.m; sourceTree = ""; }; 7A02CEEB2361511100B49FAE /* IGListAdapterUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdater.h; sourceTree = ""; }; 7A02CEEC2361511100B49FAE /* IGListSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSectionController.m; sourceTree = ""; }; 7A02CEED2361511100B49FAE /* IGListCollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListCollectionView.m; sourceTree = ""; }; 7A02CF642361513300B49FAE /* IGListDisplayHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDisplayHandler.h; sourceTree = ""; }; 7A02CF652361513300B49FAE /* IGListAdapter+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+DebugDescription.m"; sourceTree = ""; }; 7A02CF662361513400B49FAE /* IGListAdapterInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterInternal.h; sourceTree = ""; }; 7A02CF672361513400B49FAE /* IGListBindingSectionController+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListBindingSectionController+DebugDescription.h"; sourceTree = ""; }; 7A02CF682361513400B49FAE /* IGListCollectionViewLayoutInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCollectionViewLayoutInternal.h; sourceTree = ""; }; 7A02CF6A2361513400B49FAE /* UIScrollView+IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIScrollView+IGListKit.h"; sourceTree = ""; }; 7A02CF6B2361513400B49FAE /* UICollectionView+IGListBatchUpdateData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+IGListBatchUpdateData.m"; sourceTree = ""; }; 7A02CF6C2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewLayout+InteractiveReordering.h"; sourceTree = ""; }; 7A02CF6D2361513400B49FAE /* IGListReloadIndexPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListReloadIndexPath.m; sourceTree = ""; }; 7A02CF6E2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionViewLayout+InteractiveReordering.m"; sourceTree = ""; }; 7A02CF6F2361513400B49FAE /* UIScrollView+IGListKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIScrollView+IGListKit.m"; sourceTree = ""; }; 7A02CF702361513400B49FAE /* IGListAdapterUpdaterInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdaterInternal.h; sourceTree = ""; }; 7A02CF712361513400B49FAE /* IGListSectionMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListSectionMap.h; sourceTree = ""; }; 7A02CF722361513400B49FAE /* IGListAdapterProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterProxy.m; sourceTree = ""; }; 7A02CF732361513400B49FAE /* IGListAdapterUpdater+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapterUpdater+DebugDescription.m"; sourceTree = ""; }; 7A02CF742361513400B49FAE /* IGListAdapter+UICollectionView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+UICollectionView.h"; sourceTree = ""; }; 7A02CF752361513400B49FAE /* UICollectionView+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UICollectionView+DebugDescription.m"; sourceTree = ""; }; 7A02CF762361513400B49FAE /* IGListSectionMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSectionMap.m; sourceTree = ""; }; 7A02CF772361513400B49FAE /* UICollectionView+IGListBatchUpdateData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+IGListBatchUpdateData.h"; sourceTree = ""; }; 7A02CF782361513400B49FAE /* IGListBatchUpdateState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchUpdateState.h; sourceTree = ""; }; 7A02CF792361513400B49FAE /* IGListDebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDebugger.h; sourceTree = ""; }; 7A02CF7C2361513400B49FAE /* IGListBatchUpdateData+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListBatchUpdateData+DebugDescription.m"; sourceTree = ""; }; 7A02CF7D2361513400B49FAE /* IGListAdapterProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterProxy.h; sourceTree = ""; }; 7A02CF7E2361513500B49FAE /* IGListAdapterUpdater+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapterUpdater+DebugDescription.h"; sourceTree = ""; }; 7A02CF7F2361513500B49FAE /* IGListAdapter+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+DebugDescription.h"; sourceTree = ""; }; 7A02CF802361513500B49FAE /* IGListDisplayHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDisplayHandler.m; sourceTree = ""; }; 7A02CF812361513500B49FAE /* IGListArrayUtilsInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListArrayUtilsInternal.h; sourceTree = ""; }; 7A02CF832361513500B49FAE /* IGListDebuggingUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDebuggingUtilities.h; sourceTree = ""; }; 7A02CF842361513500B49FAE /* IGListBindingSectionController+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListBindingSectionController+DebugDescription.m"; sourceTree = ""; }; 7A02CF852361513500B49FAE /* IGListAdapter+UICollectionView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+UICollectionView.m"; sourceTree = ""; }; 7A02CF862361513500B49FAE /* IGListSectionMap+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListSectionMap+DebugDescription.h"; sourceTree = ""; }; 7A02CF872361513500B49FAE /* IGListWorkingRangeHandler.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = IGListWorkingRangeHandler.mm; sourceTree = ""; }; 7A02CF882361513500B49FAE /* IGListReloadIndexPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListReloadIndexPath.h; sourceTree = ""; }; 7A02CF892361513500B49FAE /* IGListDebugger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDebugger.m; sourceTree = ""; }; 7A02CF8A2361513500B49FAE /* IGListSectionControllerInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListSectionControllerInternal.h; sourceTree = ""; }; 7A02CF8B2361513500B49FAE /* IGListDebuggingUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDebuggingUtilities.m; sourceTree = ""; }; 7A02CF8C2361513500B49FAE /* IGListBatchUpdateData+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListBatchUpdateData+DebugDescription.h"; sourceTree = ""; }; 7A02CF8D2361513600B49FAE /* IGListSectionMap+DebugDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListSectionMap+DebugDescription.m"; sourceTree = ""; }; 7A02CF8E2361513600B49FAE /* IGListWorkingRangeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListWorkingRangeHandler.h; sourceTree = ""; }; 7A02CF8F2361513600B49FAE /* UICollectionView+DebugDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionView+DebugDescription.h"; sourceTree = ""; }; 7A02D01D2361520200B49FAE /* IGListDiffKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListDiffKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7A02D02B2361525800B49FAE /* IGListDiffKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListDiffKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7A02D0382361526600B49FAE /* IGListDiffKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListDiffKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7A02D0402361529E00B49FAE /* IGListDiff.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDiff.h; sourceTree = ""; }; 7A02D0412361529E00B49FAE /* NSNumber+IGListDiffable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNumber+IGListDiffable.m"; sourceTree = ""; }; 7A02D0422361529E00B49FAE /* NSString+IGListDiffable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+IGListDiffable.h"; sourceTree = ""; }; 7A02D0432361529E00B49FAE /* IGListIndexPathResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListIndexPathResult.h; sourceTree = ""; }; 7A02D0442361529E00B49FAE /* IGListDiffKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDiffKit.h; sourceTree = ""; }; 7A02D0452361529E00B49FAE /* IGListExperiments.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListExperiments.h; sourceTree = ""; }; 7A02D0462361529E00B49FAE /* NSString+IGListDiffable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+IGListDiffable.m"; sourceTree = ""; }; 7A02D0472361529E00B49FAE /* IGListCompatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListCompatibility.h; sourceTree = ""; }; 7A02D0482361529E00B49FAE /* NSNumber+IGListDiffable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNumber+IGListDiffable.h"; sourceTree = ""; }; 7A02D04A2361529E00B49FAE /* IGListIndexSetResultInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListIndexSetResultInternal.h; sourceTree = ""; }; 7A02D04B2361529E00B49FAE /* IGListIndexPathResultInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListIndexPathResultInternal.h; sourceTree = ""; }; 7A02D04C2361529E00B49FAE /* IGListMoveIndexInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListMoveIndexInternal.h; sourceTree = ""; }; 7A02D04D2361529E00B49FAE /* IGListMoveIndexPathInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListMoveIndexPathInternal.h; sourceTree = ""; }; 7A02D04E2361529E00B49FAE /* IGListMoveIndexPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListMoveIndexPath.h; sourceTree = ""; }; 7A02D04F2361529E00B49FAE /* IGListDiff.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = IGListDiff.mm; sourceTree = ""; }; 7A02D0502361529E00B49FAE /* IGListAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAssert.h; sourceTree = ""; }; 7A02D0512361529E00B49FAE /* IGListIndexPathResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListIndexPathResult.m; sourceTree = ""; }; 7A02D0522361529E00B49FAE /* IGListIndexSetResult.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListIndexSetResult.m; sourceTree = ""; }; 7A02D0532361529E00B49FAE /* IGListMoveIndex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListMoveIndex.m; sourceTree = ""; }; 7A02D0542361529E00B49FAE /* IGListBatchUpdateData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBatchUpdateData.h; sourceTree = ""; }; 7A02D0552361529E00B49FAE /* IGListMoveIndexPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListMoveIndexPath.m; sourceTree = ""; }; 7A02D0562361529E00B49FAE /* IGListDiffable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListDiffable.h; sourceTree = ""; }; 7A02D0572361529E00B49FAE /* IGListBatchUpdateData.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = IGListBatchUpdateData.mm; sourceTree = ""; }; 7A02D0582361529E00B49FAE /* IGListMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListMacros.h; sourceTree = ""; }; 7A02D0592361529E00B49FAE /* IGListMoveIndex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListMoveIndex.h; sourceTree = ""; }; 7A02D05A2361529F00B49FAE /* IGListIndexSetResult.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListIndexSetResult.h; sourceTree = ""; }; 7A02D0AC236153CE00B49FAE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7A92EA9723A8A42000E5A13D /* IGListSwiftKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListSwiftKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7A92EA9923A8A42000E5A13D /* IGListSwiftKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListSwiftKit.h; sourceTree = ""; }; 7A92EAA823A8A50100E5A13D /* IGListSwiftKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListSwiftKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AD6864923A89E7F009000DE /* IGListAdapterUpdaterHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterUpdaterHelpers.m; sourceTree = ""; }; 7AD6864A23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListAdapterUpdaterHelpers.h; sourceTree = ""; }; 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleStoryboardItemControllerTests.m; sourceTree = ""; }; 821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = IGTestStoryboard.storyboard; sourceTree = ""; }; 821BC4C71DB8D5B200172ED0 /* IGTestStoryboardViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStoryboardViewController.h; sourceTree = ""; }; 821BC4C81DB8D5B200172ED0 /* IGTestStoryboardViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestStoryboardViewController.m; sourceTree = ""; }; 821BC4CC1DB8D8C500172ED0 /* IGTestStoryboardCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStoryboardCell.h; sourceTree = ""; }; 821BC4CD1DB8D8C500172ED0 /* IGTestStoryboardCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestStoryboardCell.m; sourceTree = ""; }; 821BC4D11DB9816E00172ED0 /* IGTestSingleStoryboardItemDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestSingleStoryboardItemDataSource.h; sourceTree = ""; }; 821BC4D21DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestSingleStoryboardItemDataSource.m; sourceTree = ""; }; 8240C7EE1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStoryboardSupplementaryView.h; sourceTree = ""; }; 8240C7EF1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestStoryboardSupplementaryView.m; sourceTree = ""; }; 8240C7F11DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterStoryboardTests.m; sourceTree = ""; }; 8240C7F31DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestStoryboardSupplementarySource.h; sourceTree = ""; }; 8240C7F41DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestStoryboardSupplementarySource.m; sourceTree = ""; }; 8240C7F61DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestStoryboardSection.h; sourceTree = ""; }; 8240C7F71DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestStoryboardSection.m; sourceTree = ""; }; 8240C7F91DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestAdapterStoryboardDataSource.h; sourceTree = ""; }; 8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestAdapterStoryboardDataSource.m; sourceTree = ""; }; 8285404A1DE40C6E00118B94 /* IGListTestHorizontalSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestHorizontalSection.h; sourceTree = ""; }; 8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestHorizontalSection.m; sourceTree = ""; }; 8285404E1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestAdapterHorizontalDataSource.h; sourceTree = ""; }; 8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestAdapterHorizontalDataSource.m; sourceTree = ""; }; 82914C591E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestContainerSizeSection.h; sourceTree = ""; }; 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestContainerSizeSection.m; sourceTree = ""; }; 829D7BA81DD1816400549816 /* IGListSectionMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSectionMapTests.m; sourceTree = ""; }; 88144EE21D870EDC007C7F66 /* IGListAdapterE2ETests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterE2ETests.m; sourceTree = ""; }; 88144EE31D870EDC007C7F66 /* IGListAdapterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterTests.m; sourceTree = ""; }; 88144EE41D870EDC007C7F66 /* IGListAdapterUpdaterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterUpdaterTests.m; sourceTree = ""; }; 88144EE51D870EDC007C7F66 /* IGListBatchUpdateDataTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBatchUpdateDataTests.m; sourceTree = ""; }; 88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IGListDiffSwiftTests.swift; sourceTree = ""; }; 88144EE81D870EDC007C7F66 /* IGListDiffTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDiffTests.m; sourceTree = ""; }; 88144EE91D870EDC007C7F66 /* IGListDisplayHandlerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDisplayHandlerTests.m; sourceTree = ""; }; 88144EEB1D870EDC007C7F66 /* IGListKitTests-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListKitTests-Bridging-Header.h"; sourceTree = ""; }; 88144EED1D870EDC007C7F66 /* IGListSingleSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleSectionControllerTests.m; sourceTree = ""; }; 88144EEF1D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListWorkingRangeHandlerTests.m; sourceTree = ""; }; 88144EF11D870EDC007C7F66 /* IGListTestAdapterDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestAdapterDataSource.h; sourceTree = ""; }; 88144EF21D870EDC007C7F66 /* IGListTestAdapterDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestAdapterDataSource.m; sourceTree = ""; }; 88144EF31D870EDC007C7F66 /* IGListTestOffsettingLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestOffsettingLayout.h; sourceTree = ""; }; 88144EF41D870EDC007C7F66 /* IGListTestOffsettingLayout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestOffsettingLayout.m; sourceTree = ""; }; 88144EF51D870EDC007C7F66 /* IGListTestSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestSection.h; sourceTree = ""; }; 88144EF61D870EDC007C7F66 /* IGListTestSection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestSection.m; sourceTree = ""; }; 88144EF71D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListTestUICollectionViewDataSource.h; sourceTree = ""; }; 88144EF81D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTestUICollectionViewDataSource.m; sourceTree = ""; }; 88144EF91D870EDC007C7F66 /* IGTestCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestCell.h; sourceTree = ""; }; 88144EFA1D870EDC007C7F66 /* IGTestCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestCell.m; sourceTree = ""; }; 88144EFB1D870EDC007C7F66 /* IGTestDelegateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestDelegateController.h; sourceTree = ""; }; 88144EFC1D870EDC007C7F66 /* IGTestDelegateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestDelegateController.m; sourceTree = ""; }; 88144EFD1D870EDC007C7F66 /* IGTestDelegateDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestDelegateDataSource.h; sourceTree = ""; }; 88144EFE1D870EDC007C7F66 /* IGTestDelegateDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestDelegateDataSource.m; sourceTree = ""; }; 88144EFF1D870EDC007C7F66 /* IGTestObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestObject.h; sourceTree = ""; }; 88144F001D870EDC007C7F66 /* IGTestObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestObject.m; sourceTree = ""; }; 88144F011D870EDC007C7F66 /* IGTestSingleItemDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestSingleItemDataSource.h; sourceTree = ""; }; 88144F021D870EDC007C7F66 /* IGTestSingleItemDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestSingleItemDataSource.m; sourceTree = ""; }; 88144F051D870EDC007C7F66 /* IGTestSupplementarySource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestSupplementarySource.h; sourceTree = ""; }; 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestSupplementarySource.m; sourceTree = ""; }; 885FE1D41DC51A0D009CE2B4 /* IGListKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 885FE1DC1DC51A0D009CE2B4 /* IGListKit-tvOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "IGListKit-tvOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 887D0B361D870D7E009E01F7 /* IGListKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 887D0B3F1D870D7F009E01F7 /* IGListKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IGListKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 887D0B571D870E1E009E01F7 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88DF897C1E010E6A00B1B9B4 /* IGListKit-macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "IGListKit-macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; A46A1D1F2D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListAdapterDelegateAnnouncerInternal.h; sourceTree = ""; }; A46A1D222D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+IGListAdapterInternal.h"; sourceTree = ""; }; A46A1D252D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListAdapterDelegateAnnouncer.h; sourceTree = ""; }; A46A1D262D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListAdapterDelegateAnnouncer.m; sourceTree = ""; }; A46A1D292D80213D00CB9157 /* UIViewController+IGListAdapter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+IGListAdapter.h"; sourceTree = ""; }; A46A1D2A2D80213D00CB9157 /* UIViewController+IGListAdapter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+IGListAdapter.m"; sourceTree = ""; }; A46A1D372D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListCollectionViewLayoutInvalidationContext.h; sourceTree = ""; }; A46A1D382D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListCollectionViewLayoutInvalidationContext.m; sourceTree = ""; }; DD3152AC1DE227FA00AC9D2C /* IGListKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = IGListKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E56B7B3320A9D7100071010C /* IGListCollectionScrollingTraitsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListCollectionScrollingTraitsTests.m; sourceTree = ""; }; E8D312DF1FC472A60009FA2F /* IGListContentInsetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListContentInsetTests.m; sourceTree = ""; }; F102DBBA2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListArrayUtilsInternal.m; sourceTree = ""; }; F10C8F562B982DFD009F4690 /* IGListDefaultExperiments.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListDefaultExperiments.h; sourceTree = ""; }; F10C8F592B9830E6009F4690 /* IGListTestCollectionViewLayout.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IGListTestCollectionViewLayout.m; sourceTree = ""; }; F10C8F5A2B9830E6009F4690 /* IGListTestCollectionViewLayout.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IGListTestCollectionViewLayout.h; sourceTree = ""; }; F14291E62D83FA0A007D3437 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; F14291E72D83FA0A007D3437 /* README.zh.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.zh.md; sourceTree = ""; }; F15DB2EC2D813B090066C7AD /* IGListAdapter+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IGListAdapter+Async.swift"; sourceTree = ""; }; F15DB2ED2D813B090066C7AD /* IGListCollectionContext+Refinements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IGListCollectionContext+Refinements.swift"; sourceTree = ""; }; F15DB2EE2D813B090066C7AD /* IGListSingleSectionController+Refinements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IGListSingleSectionController+Refinements.swift"; sourceTree = ""; }; F15DB2EF2D813B090066C7AD /* ListIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListIdentifiable.swift; sourceTree = ""; }; F15DB2F02D813B090066C7AD /* ListValueSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListValueSectionController.swift; sourceTree = ""; }; F15F1DDB2BA40B84002AF3FE /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; F15F1DDC2BA40BC9002AF3FE /* IGListKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = IGListKit.podspec; sourceTree = ""; }; F15F1DDD2BA40BC9002AF3FE /* IGListDiffKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = IGListDiffKit.podspec; sourceTree = ""; }; F15F1DDE2BA40BC9002AF3FE /* IGListSwiftKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = IGListSwiftKit.podspec; sourceTree = ""; }; F1855A4B29BC565600558D18 /* IGListDiffDescriptionStringTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDiffDescriptionStringTests.m; sourceTree = ""; }; F18CC75B29EFBB9400DC3B9A /* IGListCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListCollectionViewTests.m; sourceTree = ""; }; F18CC75E29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBindingSingleSectionControllerTests.m; sourceTree = ""; }; F18CC76129EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGTestBindingSingleItemDataSource.m; sourceTree = ""; }; F18CC76229EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGTestBindingSingleItemDataSource.h; sourceTree = ""; }; F18CC76529EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListReloadDataUpdaterTests.m; sourceTree = ""; }; F18CC76829EFBD0300DC3B9A /* IGListBindingSingleSectionController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IGListBindingSingleSectionController.h; sourceTree = ""; }; F18CC76929EFBD0300DC3B9A /* IGListBindingSingleSectionController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListBindingSingleSectionController.m; sourceTree = ""; }; F1E6302229EA43080060B4D6 /* IGListSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSectionControllerTests.m; sourceTree = ""; }; F1E6581B2D83FEC100989232 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; F1ED68AE29E9B3B9003744F8 /* IGListInteractiveMovingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListInteractiveMovingTests.m; sourceTree = ""; }; F1ED68AF29E9B3B9003744F8 /* IGListDebugDescriptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListDebugDescriptionTests.m; sourceTree = ""; }; F1ED68B029E9B3B9003744F8 /* IGListTransactionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListTransactionTests.m; sourceTree = ""; }; F1ED68B129E9B3B9003744F8 /* IGListSingleSectionControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IGListSingleSectionControllerTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 7A02D01A2361520200B49FAE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0282361525800B49FAE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0352361526600B49FAE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EA9423A8A42000E5A13D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EAA523A8A50100E5A13D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1D01DC51A0D009CE2B4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0AA2361530800B49FAE /* IGListDiffKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1D91DC51A0D009CE2B4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F12D8EAC2E93853500A7DDBE /* OCMock in Frameworks */, 885FE1DD1DC51A0D009CE2B4 /* IGListKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B321D870D7E009E01F7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0AB2361530E00B49FAE /* IGListDiffKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B3C1D870D7F009E01F7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F12D8EAA2E93852600A7DDBE /* OCMock in Frameworks */, 887D0B401D870D7F009E01F7 /* IGListKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 88DF89791E010E6A00B1B9B4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F12D8EAF2E93854100A7DDBE /* OCMock in Frameworks */, 88DF89811E010E6A00B1B9B4 /* IGListKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; DD31527B1DE227FA00AC9D2C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0A9236152F500B49FAE /* IGListDiffKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 0B3B927E1E08D7F5008390ED /* Source */ = { isa = PBXGroup; children = ( 7A02D0252361522600B49FAE /* IGListDiffKit */, 7A02CEC6236150C400B49FAE /* IGListKit */, 7A92EA9823A8A42000E5A13D /* IGListSwiftKit */, 7A02D0AC236153CE00B49FAE /* Info.plist */, ); path = Source; sourceTree = ""; }; 294369AF1DB1B7AE0025F6E7 /* Assets */ = { isa = PBXGroup; children = ( 294369B01DB1B7AE0025F6E7 /* IGTestNibCell.xib */, 2904861C1DCD02140007F41D /* IGTestNibSupplementaryView.xib */, 821BC4C21DB8CAE900172ED0 /* IGTestStoryboard.storyboard */, ); path = Assets; sourceTree = ""; }; 7A02CEC6236150C400B49FAE /* IGListKit */ = { isa = PBXGroup; children = ( 7A02CF632361511700B49FAE /* Internal */, 7A02CECA2361510F00B49FAE /* IGListAdapter.h */, 7A02CEE82361511000B49FAE /* IGListAdapter.m */, 7A02CEDF2361511000B49FAE /* IGListAdapterDataSource.h */, 7A02CED22361511000B49FAE /* IGListAdapterDelegate.h */, A46A1D252D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h */, A46A1D262D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m */, 7A02CED52361511000B49FAE /* IGListAdapterMoveDelegate.h */, 7A02CEE42361511000B49FAE /* IGListAdapterPerformanceDelegate.h */, 7A02CED92361511000B49FAE /* IGListAdapterUpdateListener.h */, 7A02CEEB2361511100B49FAE /* IGListAdapterUpdater.h */, 7A02CEDE2361511000B49FAE /* IGListAdapterUpdater.m */, 7A02CEE22361511000B49FAE /* IGListAdapterUpdaterDelegate.h */, 7A02CECF2361510F00B49FAE /* IGListBatchContext.h */, 7A02CEDA2361511000B49FAE /* IGListBindable.h */, 7A02CEDC2361511000B49FAE /* IGListBindingSectionController.h */, 7A02CEEA2361511100B49FAE /* IGListBindingSectionController.m */, 7A02CED02361510F00B49FAE /* IGListBindingSectionControllerDataSource.h */, 7A02CEE92361511000B49FAE /* IGListBindingSectionControllerSelectionDelegate.h */, F18CC76829EFBD0300DC3B9A /* IGListBindingSingleSectionController.h */, F18CC76929EFBD0300DC3B9A /* IGListBindingSingleSectionController.m */, 7A02CED12361510F00B49FAE /* IGListCollectionContext.h */, 7A02CEE72361511000B49FAE /* IGListCollectionScrollingTraits.h */, 7A02CECD2361510F00B49FAE /* IGListCollectionView.h */, 7A02CEED2361511100B49FAE /* IGListCollectionView.m */, 7A02CECC2361510F00B49FAE /* IGListCollectionViewDelegateLayout.h */, 7A02CED42361511000B49FAE /* IGListCollectionViewLayout.h */, 7A02CEE02361511000B49FAE /* IGListCollectionViewLayout.mm */, 7A02CEE12361511000B49FAE /* IGListCollectionViewLayoutCompatible.h */, A46A1D372D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h */, A46A1D382D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m */, 7A02CECB2361510F00B49FAE /* IGListDisplayDelegate.h */, 7A02CECE2361510F00B49FAE /* IGListGenericSectionController.h */, 7A02CED32361511000B49FAE /* IGListGenericSectionController.m */, 7A02CED72361511000B49FAE /* IGListKit.h */, 7A02CEC72361510F00B49FAE /* IGListReloadDataUpdater.h */, 7A02CEDB2361511000B49FAE /* IGListReloadDataUpdater.m */, 7A02CEC82361510F00B49FAE /* IGListScrollDelegate.h */, 7A02CED62361511000B49FAE /* IGListSectionController.h */, 7A02CEEC2361511100B49FAE /* IGListSectionController.m */, 7A02CEE32361511000B49FAE /* IGListSingleSectionController.h */, 7A02CEE52361511000B49FAE /* IGListSingleSectionController.m */, 7A02CEE62361511000B49FAE /* IGListSupplementaryViewSource.h */, 57B22E692502AAB10055DC2F /* IGListTransitionData.h */, 57B22E662502AAB10055DC2F /* IGListTransitionData.m */, 7A02CED82361511000B49FAE /* IGListTransitionDelegate.h */, 7A02CEDD2361511000B49FAE /* IGListUpdatingDelegate.h */, 7A02CEC92361510F00B49FAE /* IGListWorkingRangeDelegate.h */, A46A1D292D80213D00CB9157 /* UIViewController+IGListAdapter.h */, A46A1D2A2D80213D00CB9157 /* UIViewController+IGListAdapter.m */, ); path = IGListKit; sourceTree = ""; }; 7A02CF632361511700B49FAE /* Internal */ = { isa = PBXGroup; children = ( 7A02CF7F2361513500B49FAE /* IGListAdapter+DebugDescription.h */, 7A02CF652361513300B49FAE /* IGListAdapter+DebugDescription.m */, 7A02CF742361513400B49FAE /* IGListAdapter+UICollectionView.h */, 7A02CF852361513500B49FAE /* IGListAdapter+UICollectionView.m */, A46A1D1F2D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h */, 7A02CF662361513400B49FAE /* IGListAdapterInternal.h */, 7A02CF7D2361513400B49FAE /* IGListAdapterProxy.h */, 7A02CF722361513400B49FAE /* IGListAdapterProxy.m */, 7A02CF7E2361513500B49FAE /* IGListAdapterUpdater+DebugDescription.h */, 7A02CF732361513400B49FAE /* IGListAdapterUpdater+DebugDescription.m */, 7AD6864A23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h */, 7AD6864923A89E7F009000DE /* IGListAdapterUpdaterHelpers.m */, 7A02CF702361513400B49FAE /* IGListAdapterUpdaterInternal.h */, 7A02CF812361513500B49FAE /* IGListArrayUtilsInternal.h */, F102DBBA2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m */, 7A02CF8C2361513500B49FAE /* IGListBatchUpdateData+DebugDescription.h */, 7A02CF7C2361513400B49FAE /* IGListBatchUpdateData+DebugDescription.m */, 7A02CF782361513400B49FAE /* IGListBatchUpdateState.h */, 57B22E7B2502AAC40055DC2F /* IGListBatchUpdateTransaction.h */, 57B22E712502AAC20055DC2F /* IGListBatchUpdateTransaction.m */, 7A02CF672361513400B49FAE /* IGListBindingSectionController+DebugDescription.h */, 7A02CF842361513500B49FAE /* IGListBindingSectionController+DebugDescription.m */, 7A02CF682361513400B49FAE /* IGListCollectionViewLayoutInternal.h */, 57B22E742502AAC30055DC2F /* IGListDataSourceChangeTransaction.h */, 57B22E792502AAC30055DC2F /* IGListDataSourceChangeTransaction.m */, 7A02CF792361513400B49FAE /* IGListDebugger.h */, 7A02CF892361513500B49FAE /* IGListDebugger.m */, 7A02CF832361513500B49FAE /* IGListDebuggingUtilities.h */, 7A02CF8B2361513500B49FAE /* IGListDebuggingUtilities.m */, F10C8F562B982DFD009F4690 /* IGListDefaultExperiments.h */, 7A02CF642361513300B49FAE /* IGListDisplayHandler.h */, 7A02CF802361513500B49FAE /* IGListDisplayHandler.m */, 57B22E7C2502AAC40055DC2F /* IGListItemUpdatesCollector.h */, 57B22E752502AAC30055DC2F /* IGListItemUpdatesCollector.m */, 576029D62C61B91D006E50E2 /* IGListPerformDiff.h */, 576029D72C61B91D006E50E2 /* IGListPerformDiff.m */, 7A02CF882361513500B49FAE /* IGListReloadIndexPath.h */, 7A02CF6D2361513400B49FAE /* IGListReloadIndexPath.m */, 57B22E762502AAC30055DC2F /* IGListReloadTransaction.h */, 57B22E7E2502AAC40055DC2F /* IGListReloadTransaction.m */, 7A02CF8A2361513500B49FAE /* IGListSectionControllerInternal.h */, 7A02CF712361513400B49FAE /* IGListSectionMap.h */, 7A02CF762361513400B49FAE /* IGListSectionMap.m */, 7A02CF862361513500B49FAE /* IGListSectionMap+DebugDescription.h */, 7A02CF8D2361513600B49FAE /* IGListSectionMap+DebugDescription.m */, 576029D82C61B91D006E50E2 /* IGListUpdateCoalescer.h */, 576029DB2C61B91D006E50E2 /* IGListUpdateCoalescer.m */, 57B22E732502AAC30055DC2F /* IGListUpdateTransactable.h */, 57B22E7D2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h */, 57B22E722502AAC30055DC2F /* IGListUpdateTransactionBuilder.m */, 576029D52C61B91D006E50E2 /* IGListViewVisibilityTracker.h */, 576029D92C61B91D006E50E2 /* IGListViewVisibilityTracker.m */, 576029DA2C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h */, 7A02CF8E2361513600B49FAE /* IGListWorkingRangeHandler.h */, 7A02CF872361513500B49FAE /* IGListWorkingRangeHandler.mm */, 7A02CF8F2361513600B49FAE /* UICollectionView+DebugDescription.h */, 7A02CF752361513400B49FAE /* UICollectionView+DebugDescription.m */, 7A02CF772361513400B49FAE /* UICollectionView+IGListBatchUpdateData.h */, 7A02CF6B2361513400B49FAE /* UICollectionView+IGListBatchUpdateData.m */, 7A02CF6C2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.h */, 7A02CF6E2361513400B49FAE /* UICollectionViewLayout+InteractiveReordering.m */, 7A02CF6A2361513400B49FAE /* UIScrollView+IGListKit.h */, 7A02CF6F2361513400B49FAE /* UIScrollView+IGListKit.m */, A46A1D222D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h */, ); path = Internal; sourceTree = ""; }; 7A02D0252361522600B49FAE /* IGListDiffKit */ = { isa = PBXGroup; children = ( 7A02D0492361529E00B49FAE /* Internal */, 7A02D0502361529E00B49FAE /* IGListAssert.h */, 7A02D0542361529E00B49FAE /* IGListBatchUpdateData.h */, 7A02D0572361529E00B49FAE /* IGListBatchUpdateData.mm */, 7A02D0472361529E00B49FAE /* IGListCompatibility.h */, 7A02D0402361529E00B49FAE /* IGListDiff.h */, 7A02D04F2361529E00B49FAE /* IGListDiff.mm */, 7A02D0562361529E00B49FAE /* IGListDiffable.h */, 7A02D0442361529E00B49FAE /* IGListDiffKit.h */, 7A02D0452361529E00B49FAE /* IGListExperiments.h */, 7A02D0432361529E00B49FAE /* IGListIndexPathResult.h */, 7A02D0512361529E00B49FAE /* IGListIndexPathResult.m */, 7A02D05A2361529F00B49FAE /* IGListIndexSetResult.h */, 7A02D0522361529E00B49FAE /* IGListIndexSetResult.m */, 7A02D0582361529E00B49FAE /* IGListMacros.h */, 7A02D0592361529E00B49FAE /* IGListMoveIndex.h */, 7A02D0532361529E00B49FAE /* IGListMoveIndex.m */, 7A02D04E2361529E00B49FAE /* IGListMoveIndexPath.h */, 7A02D0552361529E00B49FAE /* IGListMoveIndexPath.m */, 7A02D0482361529E00B49FAE /* NSNumber+IGListDiffable.h */, 7A02D0412361529E00B49FAE /* NSNumber+IGListDiffable.m */, 7A02D0422361529E00B49FAE /* NSString+IGListDiffable.h */, 7A02D0462361529E00B49FAE /* NSString+IGListDiffable.m */, ); path = IGListDiffKit; sourceTree = ""; }; 7A02D0492361529E00B49FAE /* Internal */ = { isa = PBXGroup; children = ( 7A02D04A2361529E00B49FAE /* IGListIndexSetResultInternal.h */, 7A02D04B2361529E00B49FAE /* IGListIndexPathResultInternal.h */, 7A02D04C2361529E00B49FAE /* IGListMoveIndexInternal.h */, 7A02D04D2361529E00B49FAE /* IGListMoveIndexPathInternal.h */, ); path = Internal; sourceTree = ""; }; 7A92EA9823A8A42000E5A13D /* IGListSwiftKit */ = { isa = PBXGroup; children = ( F15DB2EC2D813B090066C7AD /* IGListAdapter+Async.swift */, F15DB2ED2D813B090066C7AD /* IGListCollectionContext+Refinements.swift */, F15DB2EE2D813B090066C7AD /* IGListSingleSectionController+Refinements.swift */, 7A92EA9923A8A42000E5A13D /* IGListSwiftKit.h */, F15DB2EF2D813B090066C7AD /* ListIdentifiable.swift */, F15DB2F02D813B090066C7AD /* ListValueSectionController.swift */, ); path = IGListSwiftKit; sourceTree = ""; }; 88144EF01D870EDC007C7F66 /* Objects */ = { isa = PBXGroup; children = ( 298DDA261E3B166100F76F50 /* IGLayoutTestDataSource.h */, 298DDA271E3B166100F76F50 /* IGLayoutTestDataSource.m */, 298DDA281E3B166100F76F50 /* IGLayoutTestItem.h */, 298DDA291E3B166100F76F50 /* IGLayoutTestItem.m */, 298DDA2A1E3B166100F76F50 /* IGLayoutTestSection.h */, 298DDA2B1E3B166100F76F50 /* IGLayoutTestSection.m */, 294CDE611F995DD7002CF6E4 /* IGListAdapterUpdateTester.h */, 294CDE621F995DD7002CF6E4 /* IGListAdapterUpdateTester.m */, 88144EF11D870EDC007C7F66 /* IGListTestAdapterDataSource.h */, 88144EF21D870EDC007C7F66 /* IGListTestAdapterDataSource.m */, 8285404E1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.h */, 8285404F1DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m */, 13DF01711FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.h */, 13DF01721FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m */, 8240C7F91DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.h */, 8240C7FA1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m */, 82914C591E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.h */, 82914C5A1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m */, 8285404A1DE40C6E00118B94 /* IGListTestHorizontalSection.h */, 8285404B1DE40C6E00118B94 /* IGListTestHorizontalSection.m */, 88144EF31D870EDC007C7F66 /* IGListTestOffsettingLayout.h */, 88144EF41D870EDC007C7F66 /* IGListTestOffsettingLayout.m */, 88144EF51D870EDC007C7F66 /* IGListTestSection.h */, 88144EF61D870EDC007C7F66 /* IGListTestSection.m */, 8240C7F61DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.h */, 8240C7F71DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m */, 88144EF71D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.h */, 88144EF81D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m */, F18CC76229EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.h */, F18CC76129EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m */, 2995409A1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.h */, 2995409B1F588C8D00F647CF /* IGTestBindingWithoutDeselectionDelegate.m */, 88144EF91D870EDC007C7F66 /* IGTestCell.h */, 88144EFA1D870EDC007C7F66 /* IGTestCell.m */, F10C8F5A2B9830E6009F4690 /* IGListTestCollectionViewLayout.h */, F10C8F592B9830E6009F4690 /* IGListTestCollectionViewLayout.m */, 88144EFB1D870EDC007C7F66 /* IGTestDelegateController.h */, 88144EFC1D870EDC007C7F66 /* IGTestDelegateController.m */, 88144EFD1D870EDC007C7F66 /* IGTestDelegateDataSource.h */, 88144EFE1D870EDC007C7F66 /* IGTestDelegateDataSource.m */, 298DDA0B1E3AE3ED00F76F50 /* IGTestDiffingDataSource.h */, 298DDA0C1E3AE3ED00F76F50 /* IGTestDiffingDataSource.m */, 298DD9F81E3AE1AA00F76F50 /* IGTestDiffingObject.h */, 298DD9F91E3AE1AA00F76F50 /* IGTestDiffingObject.m */, 298DD9D01E3ADDB400F76F50 /* IGTestDiffingSectionController.h */, 298DD9D11E3ADDB400F76F50 /* IGTestDiffingSectionController.m */, 16B71CE522B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.h */, 16B71CE822B0A08300FE96ED /* IGTestInvalidateLayoutDataSource.m */, 16B71CE922B0A08300FE96ED /* IGTestInvalidateLayoutObject.h */, 16B71CEA22B0A08300FE96ED /* IGTestInvalidateLayoutObject.m */, 16B71CE622B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.h */, 16B71CE722B0A08300FE96ED /* IGTestInvalidateLayoutSectionController.m */, 2904861E1DCD02750007F41D /* IGTestNibSupplementaryView.h */, 2904861F1DCD02750007F41D /* IGTestNibSupplementaryView.m */, 298DD9E01E3ADE4300F76F50 /* IGTestNumberBindableCell.h */, 298DD9E11E3ADE4300F76F50 /* IGTestNumberBindableCell.m */, 88144EFF1D870EDC007C7F66 /* IGTestObject.h */, 88144F001D870EDC007C7F66 /* IGTestObject.m */, 13DF01751FA1000E0092A320 /* IGTestReorderableSection.h */, 13DF01761FA1000E0092A320 /* IGTestReorderableSection.m */, 88144F011D870EDC007C7F66 /* IGTestSingleItemDataSource.h */, 88144F021D870EDC007C7F66 /* IGTestSingleItemDataSource.m */, 26271C881DAE94E40073E116 /* IGTestSingleNibItemDataSource.h */, 26271C891DAE94E40073E116 /* IGTestSingleNibItemDataSource.m */, 821BC4D11DB9816E00172ED0 /* IGTestSingleStoryboardItemDataSource.h */, 821BC4D21DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m */, 6A9EB35F1F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.h */, 6A9EB3601F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m */, 821BC4CC1DB8D8C500172ED0 /* IGTestStoryboardCell.h */, 821BC4CD1DB8D8C500172ED0 /* IGTestStoryboardCell.m */, 8240C7F31DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.h */, 8240C7F41DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m */, 8240C7EE1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.h */, 8240C7EF1DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m */, 821BC4C71DB8D5B200172ED0 /* IGTestStoryboardViewController.h */, 821BC4C81DB8D5B200172ED0 /* IGTestStoryboardViewController.m */, 298DD9D81E3ADE3300F76F50 /* IGTestStringBindableCell.h */, 298DD9D91E3ADE3300F76F50 /* IGTestStringBindableCell.m */, 88144F051D870EDC007C7F66 /* IGTestSupplementarySource.h */, 88144F061D870EDC007C7F66 /* IGTestSupplementarySource.m */, ); path = Objects; sourceTree = ""; }; 887D0B2C1D870D7E009E01F7 = { isa = PBXGroup; children = ( 0B3B927E1E08D7F5008390ED /* Source */, 887D0B551D870E1E009E01F7 /* Tests */, 887D0B371D870D7E009E01F7 /* Products */, F15F1DDD2BA40BC9002AF3FE /* IGListDiffKit.podspec */, F15F1DDC2BA40BC9002AF3FE /* IGListKit.podspec */, F15F1DDE2BA40BC9002AF3FE /* IGListSwiftKit.podspec */, F15F1DDB2BA40B84002AF3FE /* Package.swift */, F14291E62D83FA0A007D3437 /* README.md */, F14291E72D83FA0A007D3437 /* README.zh.md */, F1E6581B2D83FEC100989232 /* CHANGELOG.md */, F12D8EAD2E93854100A7DDBE /* Frameworks */, ); sourceTree = ""; usesTabs = 0; }; 887D0B371D870D7E009E01F7 /* Products */ = { isa = PBXGroup; children = ( 887D0B361D870D7E009E01F7 /* IGListKit.framework */, 887D0B3F1D870D7F009E01F7 /* IGListKitTests.xctest */, 885FE1D41DC51A0D009CE2B4 /* IGListKit.framework */, 885FE1DC1DC51A0D009CE2B4 /* IGListKit-tvOSTests.xctest */, DD3152AC1DE227FA00AC9D2C /* IGListKit.framework */, 88DF897C1E010E6A00B1B9B4 /* IGListKit-macOSTests.xctest */, 7A02D01D2361520200B49FAE /* IGListDiffKit.framework */, 7A02D02B2361525800B49FAE /* IGListDiffKit.framework */, 7A02D0382361526600B49FAE /* IGListDiffKit.framework */, 7A92EA9723A8A42000E5A13D /* IGListSwiftKit.framework */, 7A92EAA823A8A50100E5A13D /* IGListSwiftKit.framework */, ); name = Products; sourceTree = ""; }; 887D0B551D870E1E009E01F7 /* Tests */ = { isa = PBXGroup; children = ( 294369AF1DB1B7AE0025F6E7 /* Assets */, 88144EE21D870EDC007C7F66 /* IGListAdapterE2ETests.m */, 5766613D2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m */, 29C4748A1DDF45E700AE68CE /* IGListAdapterProxyTests.m */, 8240C7F11DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m */, 88144EE31D870EDC007C7F66 /* IGListAdapterTests.m */, 88144EE41D870EDC007C7F66 /* IGListAdapterUpdaterTests.m */, 88144EE51D870EDC007C7F66 /* IGListBatchUpdateDataTests.m */, 298DD9CD1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m */, F18CC75E29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m */, E56B7B3320A9D7100071010C /* IGListCollectionScrollingTraitsTests.m */, F1ED68B129E9B3B9003744F8 /* IGListSingleSectionControllerTests.m */, 298DDA231E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m */, F18CC75B29EFBB9400DC3B9A /* IGListCollectionViewTests.m */, E8D312DF1FC472A60009FA2F /* IGListContentInsetTests.m */, F1ED68AF29E9B3B9003744F8 /* IGListDebugDescriptionTests.m */, 290DF3761E9323E6009FE456 /* IGListDebuggerTests.m */, F1855A4B29BC565600558D18 /* IGListDiffDescriptionStringTests.m */, 294AC6311DDE4C19002FCE5D /* IGListDiffResultTests.m */, 88144EE61D870EDC007C7F66 /* IGListDiffSwiftTests.swift */, 88144EE81D870EDC007C7F66 /* IGListDiffTests.m */, 88144EE91D870EDC007C7F66 /* IGListDisplayHandlerTests.m */, 29DA5CA21EA7C72400113926 /* IGListGenericSectionControllerTests.m */, F1ED68AE29E9B3B9003744F8 /* IGListInteractiveMovingTests.m */, 22907AC02F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m */, 88144EEB1D870EDC007C7F66 /* IGListKitTests-Bridging-Header.h */, 22907AC92F2870020015F3D0 /* IGListPerformDiffTests.m */, F18CC76529EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m */, F1E6302229EA43080060B4D6 /* IGListSectionControllerTests.m */, 829D7BA81DD1816400549816 /* IGListSectionMapTests.m */, 26271C8B1DAE96740073E116 /* IGListSingleNibItemControllerTests.m */, 88144EED1D870EDC007C7F66 /* IGListSingleSectionControllerTests.m */, 821BC4BE1DB8C95300172ED0 /* IGListSingleStoryboardItemControllerTests.m */, 29DA5CA91EA7D39B00113926 /* IGListTestCase.h */, 29DA5CA61EA7D37000113926 /* IGListTestCase.m */, 29DA5CAA1EA7D3FF00113926 /* IGListTestHelpers.h */, F1ED68B029E9B3B9003744F8 /* IGListTransactionTests.m */, 22907AC32F2866160015F3D0 /* IGListUpdateCoalescerTests.m */, 88144EEF1D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m */, 22907ABC2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m */, 22907AC62F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m */, 887D0B571D870E1E009E01F7 /* Info.plist */, 88144EF01D870EDC007C7F66 /* Objects */, ); path = Tests; sourceTree = ""; }; F12D8EAD2E93854100A7DDBE /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 7A02D0182361520200B49FAE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0A32361529F00B49FAE /* IGListMoveIndex.h in Headers */, 7A02D0762361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */, 7A02D07C2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */, 7A02D09A2361529F00B49FAE /* IGListDiffable.h in Headers */, 7A02D0732361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */, 7A02D05B2361529F00B49FAE /* IGListDiff.h in Headers */, 7A02D0A02361529F00B49FAE /* IGListMacros.h in Headers */, 7A02D0642361529F00B49FAE /* IGListIndexPathResult.h in Headers */, 7A02D0792361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */, 7A02D06A2361529F00B49FAE /* IGListExperiments.h in Headers */, 7A02D0822361529F00B49FAE /* IGListMoveIndexPath.h in Headers */, 7A02D0942361529F00B49FAE /* IGListBatchUpdateData.h in Headers */, 7A02D07F2361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */, 7A02D0702361529F00B49FAE /* IGListCompatibility.h in Headers */, 7A02D0882361529F00B49FAE /* IGListAssert.h in Headers */, 7A02D0A62361529F00B49FAE /* IGListIndexSetResult.h in Headers */, 7A02D0672361529F00B49FAE /* IGListDiffKit.h in Headers */, 7A02D0612361529F00B49FAE /* NSString+IGListDiffable.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0262361525800B49FAE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0682361529F00B49FAE /* IGListDiffKit.h in Headers */, 7A02D06B2361529F00B49FAE /* IGListExperiments.h in Headers */, 7A02D0622361529F00B49FAE /* NSString+IGListDiffable.h in Headers */, 7A02D0952361529F00B49FAE /* IGListBatchUpdateData.h in Headers */, 7A02D0A12361529F00B49FAE /* IGListMacros.h in Headers */, 7A02D0772361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */, 7A02D0802361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */, 7A02D07A2361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */, 7A02D0652361529F00B49FAE /* IGListIndexPathResult.h in Headers */, 7A02D0712361529F00B49FAE /* IGListCompatibility.h in Headers */, 7A02D0A42361529F00B49FAE /* IGListMoveIndex.h in Headers */, 7A02D0742361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */, 7A02D0832361529F00B49FAE /* IGListMoveIndexPath.h in Headers */, 7A02D05C2361529F00B49FAE /* IGListDiff.h in Headers */, 7A02D0892361529F00B49FAE /* IGListAssert.h in Headers */, 7A02D07D2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */, 7A02D09B2361529F00B49FAE /* IGListDiffable.h in Headers */, 7A02D0A72361529F00B49FAE /* IGListIndexSetResult.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0332361526600B49FAE /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A02D0692361529F00B49FAE /* IGListDiffKit.h in Headers */, 7A02D06C2361529F00B49FAE /* IGListExperiments.h in Headers */, 7A02D0632361529F00B49FAE /* NSString+IGListDiffable.h in Headers */, 7A02D0962361529F00B49FAE /* IGListBatchUpdateData.h in Headers */, 7A02D0A22361529F00B49FAE /* IGListMacros.h in Headers */, 7A02D0782361529F00B49FAE /* IGListIndexSetResultInternal.h in Headers */, 7A02D0812361529F00B49FAE /* IGListMoveIndexPathInternal.h in Headers */, 7A02D07B2361529F00B49FAE /* IGListIndexPathResultInternal.h in Headers */, 7A02D0662361529F00B49FAE /* IGListIndexPathResult.h in Headers */, 7A02D0722361529F00B49FAE /* IGListCompatibility.h in Headers */, 7A02D0A52361529F00B49FAE /* IGListMoveIndex.h in Headers */, 7A02D0752361529F00B49FAE /* NSNumber+IGListDiffable.h in Headers */, 7A02D0842361529F00B49FAE /* IGListMoveIndexPath.h in Headers */, 7A02D05D2361529F00B49FAE /* IGListDiff.h in Headers */, 7A02D08A2361529F00B49FAE /* IGListAssert.h in Headers */, 7A02D07E2361529F00B49FAE /* IGListMoveIndexInternal.h in Headers */, 7A02D09C2361529F00B49FAE /* IGListDiffable.h in Headers */, 7A02D0A82361529F00B49FAE /* IGListIndexSetResult.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EA9223A8A42000E5A13D /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A92EA9B23A8A42000E5A13D /* IGListSwiftKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EAA323A8A50100E5A13D /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A92EAB023A8A50C00E5A13D /* IGListSwiftKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1D11DC51A0D009CE2B4 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( A46A1D212D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h in Headers */, 7A02CF102361511100B49FAE /* IGListAdapterDelegate.h in Headers */, 7A02CFB52361513600B49FAE /* IGListAdapterUpdaterInternal.h in Headers */, 7A02CF222361511100B49FAE /* IGListTransitionDelegate.h in Headers */, 7A02CEEF2361511100B49FAE /* IGListReloadDataUpdater.h in Headers */, 7A02CFB82361513600B49FAE /* IGListSectionMap.h in Headers */, A46A1D2E2D80213D00CB9157 /* UIViewController+IGListAdapter.h in Headers */, A46A1D302D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h in Headers */, F18CC76B29EFBD0300DC3B9A /* IGListBindingSingleSectionController.h in Headers */, 7A02CF432361511100B49FAE /* IGListSingleSectionController.h in Headers */, A46A1D232D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h in Headers */, 7A02CF1F2361511100B49FAE /* IGListKit.h in Headers */, 7A02CFE22361513600B49FAE /* IGListAdapter+DebugDescription.h in Headers */, 7A02CF4F2361511100B49FAE /* IGListCollectionScrollingTraits.h in Headers */, 7A02CF192361511100B49FAE /* IGListAdapterMoveDelegate.h in Headers */, 7A02CF972361513600B49FAE /* IGListAdapterInternal.h in Headers */, 7A02CFD02361513600B49FAE /* IGListDebugger.h in Headers */, 7A02CF2E2361511100B49FAE /* IGListBindingSectionController.h in Headers */, 7A02CF3D2361511100B49FAE /* IGListCollectionViewLayoutCompatible.h in Headers */, 7A02CF402361511100B49FAE /* IGListAdapterUpdaterDelegate.h in Headers */, 7A02CEFE2361511100B49FAE /* IGListCollectionViewDelegateLayout.h in Headers */, 7A02CF5B2361511100B49FAE /* IGListAdapterUpdater.h in Headers */, 7A02CF252361511100B49FAE /* IGListAdapterUpdateListener.h in Headers */, 7A02D00F2361513600B49FAE /* IGListWorkingRangeHandler.h in Headers */, 7A02CFA32361513600B49FAE /* UIScrollView+IGListKit.h in Headers */, 7A02CEF52361511100B49FAE /* IGListWorkingRangeDelegate.h in Headers */, 7A02CEF82361511100B49FAE /* IGListAdapter.h in Headers */, 576029DD2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */, 7A02CFDF2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.h in Headers */, 7A02CF042361511100B49FAE /* IGListGenericSectionController.h in Headers */, 7A02CFFD2361513600B49FAE /* IGListReloadIndexPath.h in Headers */, 576029E72C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h in Headers */, 7A02CEFB2361511100B49FAE /* IGListDisplayDelegate.h in Headers */, 7A02CF4C2361511100B49FAE /* IGListSupplementaryViewSource.h in Headers */, 7A02CFA92361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.h in Headers */, E03DEA83255C9AAC00ACCAFC /* IGListTransitionData.h in Headers */, 0A89290026CDA632003FABD8 /* IGListBatchUpdateTransaction.h in Headers */, F10C8F582B982EE7009F4690 /* IGListDefaultExperiments.h in Headers */, 0A8928FB26CDA591003FABD8 /* IGListReloadTransaction.h in Headers */, 0A8928FA26CDA53B003FABD8 /* IGListUpdateTransactionBuilder.h in Headers */, 0A8928FE26CDA5EA003FABD8 /* IGListDataSourceChangeTransaction.h in Headers */, 7A02CF462361511100B49FAE /* IGListAdapterPerformanceDelegate.h in Headers */, 7A02CF0A2361511100B49FAE /* IGListBindingSectionControllerDataSource.h in Headers */, 7A02D0122361513600B49FAE /* UICollectionView+DebugDescription.h in Headers */, 0A89290126CDA666003FABD8 /* IGListItemUpdatesCollector.h in Headers */, 7A92EAB423A8A5FA00E5A13D /* IGListAdapterUpdaterHelpers.h in Headers */, 576029DF2C61B91D006E50E2 /* IGListPerformDiff.h in Headers */, 7A02CF0D2361511100B49FAE /* IGListCollectionContext.h in Headers */, 7A02CF372361511100B49FAE /* IGListAdapterDataSource.h in Headers */, 7A02CFC12361513600B49FAE /* IGListAdapter+UICollectionView.h in Headers */, 7A02CFDC2361513600B49FAE /* IGListAdapterProxy.h in Headers */, 7A02CFCD2361513600B49FAE /* IGListBatchUpdateState.h in Headers */, 7A02CEF22361511100B49FAE /* IGListScrollDelegate.h in Headers */, 7A02CF9A2361513600B49FAE /* IGListBindingSectionController+DebugDescription.h in Headers */, 7A02CF9D2361513600B49FAE /* IGListCollectionViewLayoutInternal.h in Headers */, 7A02CFCA2361513600B49FAE /* UICollectionView+IGListBatchUpdateData.h in Headers */, 7A02D0092361513600B49FAE /* IGListBatchUpdateData+DebugDescription.h in Headers */, 7A02CFEE2361513600B49FAE /* IGListDebuggingUtilities.h in Headers */, 576029E32C61B91D006E50E2 /* IGListUpdateCoalescer.h in Headers */, 7A02D0032361513600B49FAE /* IGListSectionControllerInternal.h in Headers */, A46A1D3C2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h in Headers */, 7A02CF162361511100B49FAE /* IGListCollectionViewLayout.h in Headers */, 7A02CF072361511100B49FAE /* IGListBatchContext.h in Headers */, 7A02CF312361511100B49FAE /* IGListUpdatingDelegate.h in Headers */, 7A02CFE82361513600B49FAE /* IGListArrayUtilsInternal.h in Headers */, 7A02CFF72361513600B49FAE /* IGListSectionMap+DebugDescription.h in Headers */, 7A02CF552361511100B49FAE /* IGListBindingSectionControllerSelectionDelegate.h in Headers */, 7A02CF282361511100B49FAE /* IGListBindable.h in Headers */, 7A02CF1C2361511100B49FAE /* IGListSectionController.h in Headers */, 7A02CF912361513600B49FAE /* IGListDisplayHandler.h in Headers */, 7A02CF012361511100B49FAE /* IGListCollectionView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B331D870D7E009E01F7 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 7A02CEEE2361511100B49FAE /* IGListReloadDataUpdater.h in Headers */, 7A02CF212361511100B49FAE /* IGListTransitionDelegate.h in Headers */, 576029E62C61B91D006E50E2 /* IGListViewVisibilityTrackerInternal.h in Headers */, 7A02CF362361511100B49FAE /* IGListAdapterDataSource.h in Headers */, 7A02CF962361513600B49FAE /* IGListAdapterInternal.h in Headers */, 7A02CF062361511100B49FAE /* IGListBatchContext.h in Headers */, 7A02D0112361513600B49FAE /* UICollectionView+DebugDescription.h in Headers */, 57B22E8A2502AAC40055DC2F /* IGListItemUpdatesCollector.h in Headers */, 7A02CFB72361513600B49FAE /* IGListSectionMap.h in Headers */, 7A02CF3C2361511100B49FAE /* IGListCollectionViewLayoutCompatible.h in Headers */, 7A02CFC02361513600B49FAE /* IGListAdapter+UICollectionView.h in Headers */, 7A02CF4B2361511100B49FAE /* IGListSupplementaryViewSource.h in Headers */, 7A02CEF72361511100B49FAE /* IGListAdapter.h in Headers */, 57B22E842502AAC40055DC2F /* IGListReloadTransaction.h in Headers */, 7A02CF182361511100B49FAE /* IGListAdapterMoveDelegate.h in Headers */, 7A02CF1E2361511100B49FAE /* IGListKit.h in Headers */, 57B22E8B2502AAC40055DC2F /* IGListUpdateTransactionBuilder.h in Headers */, 7A02CFDB2361513600B49FAE /* IGListAdapterProxy.h in Headers */, 7A02CF902361513600B49FAE /* IGListDisplayHandler.h in Headers */, 57B22E892502AAC40055DC2F /* IGListBatchUpdateTransaction.h in Headers */, 576029E22C61B91D006E50E2 /* IGListUpdateCoalescer.h in Headers */, 7A02CF0C2361511100B49FAE /* IGListCollectionContext.h in Headers */, 7A02CF0F2361511100B49FAE /* IGListAdapterDelegate.h in Headers */, 7A02CFB42361513600B49FAE /* IGListAdapterUpdaterInternal.h in Headers */, 7A02CFE72361513600B49FAE /* IGListArrayUtilsInternal.h in Headers */, 7A02CF2D2361511100B49FAE /* IGListBindingSectionController.h in Headers */, A46A1D242D80210D00CB9157 /* UIViewController+IGListAdapterInternal.h in Headers */, 7A02D00E2361513600B49FAE /* IGListWorkingRangeHandler.h in Headers */, 7A02CF992361513600B49FAE /* IGListBindingSectionController+DebugDescription.h in Headers */, 7A02CF4E2361511100B49FAE /* IGListCollectionScrollingTraits.h in Headers */, 7A02CFA22361513600B49FAE /* UIScrollView+IGListKit.h in Headers */, 57B22E6F2502AAB20055DC2F /* IGListTransitionData.h in Headers */, 7A02CF5A2361511100B49FAE /* IGListAdapterUpdater.h in Headers */, 7A02CFA82361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.h in Headers */, 7A02CF452361511100B49FAE /* IGListAdapterPerformanceDelegate.h in Headers */, 7A02D0022361513600B49FAE /* IGListSectionControllerInternal.h in Headers */, 7A02CFC92361513600B49FAE /* UICollectionView+IGListBatchUpdateData.h in Headers */, A46A1D3A2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.h in Headers */, F10C8F572B982EE6009F4690 /* IGListDefaultExperiments.h in Headers */, 7A02CEF12361511100B49FAE /* IGListScrollDelegate.h in Headers */, 7A02CFF62361513600B49FAE /* IGListSectionMap+DebugDescription.h in Headers */, F18CC76A29EFBD0300DC3B9A /* IGListBindingSingleSectionController.h in Headers */, 7AD6864C23A89E7F009000DE /* IGListAdapterUpdaterHelpers.h in Headers */, 7A02CF002361511100B49FAE /* IGListCollectionView.h in Headers */, A46A1D342D80213D00CB9157 /* UIViewController+IGListAdapter.h in Headers */, A46A1D362D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.h in Headers */, 7A02CF152361511100B49FAE /* IGListCollectionViewLayout.h in Headers */, 57B22E812502AAC40055DC2F /* IGListUpdateTransactable.h in Headers */, 7A02CF1B2361511100B49FAE /* IGListSectionController.h in Headers */, 7A02CFDE2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.h in Headers */, 7A02CEFA2361511100B49FAE /* IGListDisplayDelegate.h in Headers */, 7A02CF242361511100B49FAE /* IGListAdapterUpdateListener.h in Headers */, 576029DC2C61B91D006E50E2 /* IGListViewVisibilityTracker.h in Headers */, 7A02CF9C2361513600B49FAE /* IGListCollectionViewLayoutInternal.h in Headers */, 7A02CFED2361513600B49FAE /* IGListDebuggingUtilities.h in Headers */, 7A02CEFD2361511100B49FAE /* IGListCollectionViewDelegateLayout.h in Headers */, 7A02CF272361511100B49FAE /* IGListBindable.h in Headers */, 7A02CFCF2361513600B49FAE /* IGListDebugger.h in Headers */, 7A02CF3F2361511100B49FAE /* IGListAdapterUpdaterDelegate.h in Headers */, 7A02D0082361513600B49FAE /* IGListBatchUpdateData+DebugDescription.h in Headers */, A46A1D202D8020EF00CB9157 /* IGListAdapterDelegateAnnouncerInternal.h in Headers */, 7A02CF542361511100B49FAE /* IGListBindingSectionControllerSelectionDelegate.h in Headers */, 7A02CFCC2361513600B49FAE /* IGListBatchUpdateState.h in Headers */, 57B22E822502AAC40055DC2F /* IGListDataSourceChangeTransaction.h in Headers */, 7A02CF302361511100B49FAE /* IGListUpdatingDelegate.h in Headers */, 7A02CEF42361511100B49FAE /* IGListWorkingRangeDelegate.h in Headers */, 7A02CF092361511100B49FAE /* IGListBindingSectionControllerDataSource.h in Headers */, 7A02CF422361511100B49FAE /* IGListSingleSectionController.h in Headers */, 7A02CFE12361513600B49FAE /* IGListAdapter+DebugDescription.h in Headers */, 7A02CF032361511100B49FAE /* IGListGenericSectionController.h in Headers */, 576029DE2C61B91D006E50E2 /* IGListPerformDiff.h in Headers */, 7A02CFFC2361513600B49FAE /* IGListReloadIndexPath.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; DD31527C1DE227FA00AC9D2C /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 883797082022304E00B94676 /* (null) in Headers */, 7A02D0C023615CE500B49FAE /* IGListKit.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 7A02D01C2361520200B49FAE /* IGListDiffKit */ = { isa = PBXNativeTarget; buildConfigurationList = 7A02D0222361520200B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit" */; buildPhases = ( F15F1DE52BA40D37002AF3FE /* Copy Privacy Manifest */, 7A02D0182361520200B49FAE /* Headers */, 7A02D0192361520200B49FAE /* Sources */, 7A02D01A2361520200B49FAE /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = IGListDiffKit; productName = IGListDiffKit; productReference = 7A02D01D2361520200B49FAE /* IGListDiffKit.framework */; productType = "com.apple.product-type.framework"; }; 7A02D02A2361525800B49FAE /* IGListDiffKit-tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = 7A02D0302361525800B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit-tvOS" */; buildPhases = ( F15F1DE72BA40D5E002AF3FE /* Copy Privacy Manifest */, 7A02D0262361525800B49FAE /* Headers */, 7A02D0272361525800B49FAE /* Sources */, 7A02D0282361525800B49FAE /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "IGListDiffKit-tvOS"; productName = "IGListDiffKit-tvOS"; productReference = 7A02D02B2361525800B49FAE /* IGListDiffKit.framework */; productType = "com.apple.product-type.framework"; }; 7A02D0372361526600B49FAE /* IGListDiffKit-macOS */ = { isa = PBXNativeTarget; buildConfigurationList = 7A02D03D2361526600B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit-macOS" */; buildPhases = ( F15F1DE92BA40D6D002AF3FE /* Copy Privacy Manifest */, 7A02D0332361526600B49FAE /* Headers */, 7A02D0342361526600B49FAE /* Sources */, 7A02D0352361526600B49FAE /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = "IGListDiffKit-macOS"; productName = "IGListDiffKit-macOS"; productReference = 7A02D0382361526600B49FAE /* IGListDiffKit.framework */; productType = "com.apple.product-type.framework"; }; 7A92EA9623A8A42000E5A13D /* IGListSwiftKit */ = { isa = PBXNativeTarget; buildConfigurationList = 7A92EA9C23A8A42000E5A13D /* Build configuration list for PBXNativeTarget "IGListSwiftKit" */; buildPhases = ( F15F1DEB2BA40D7C002AF3FE /* Copy Privacy Manifest */, 7A92EA9223A8A42000E5A13D /* Headers */, 7A92EA9323A8A42000E5A13D /* Sources */, 7A92EA9423A8A42000E5A13D /* Frameworks */, 7A92EA9523A8A42000E5A13D /* Resources */, F105D9AE29B7193000B7F361 /* SwiftLint */, ); buildRules = ( ); dependencies = ( 7A92EAA223A8A4E700E5A13D /* PBXTargetDependency */, ); name = IGListSwiftKit; productName = IGListSwiftKit; productReference = 7A92EA9723A8A42000E5A13D /* IGListSwiftKit.framework */; productType = "com.apple.product-type.framework"; }; 7A92EAA723A8A50100E5A13D /* IGListSwiftKit-tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = 7A92EAAD23A8A50100E5A13D /* Build configuration list for PBXNativeTarget "IGListSwiftKit-tvOS" */; buildPhases = ( F15F1DED2BA40D94002AF3FE /* Copy Privacy Manifest */, 7A92EAA323A8A50100E5A13D /* Headers */, 7A92EAA423A8A50100E5A13D /* Sources */, 7A92EAA523A8A50100E5A13D /* Frameworks */, 7A92EAA623A8A50100E5A13D /* Resources */, F105D9AF29B7194300B7F361 /* SwiftLint */, ); buildRules = ( ); dependencies = ( 7A92EAB323A8A53F00E5A13D /* PBXTargetDependency */, ); name = "IGListSwiftKit-tvOS"; productName = "IGListSwiftKit-tvOS"; productReference = 7A92EAA823A8A50100E5A13D /* IGListSwiftKit.framework */; productType = "com.apple.product-type.framework"; }; 885FE1D31DC51A0D009CE2B4 /* IGListKit-tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = 885FE1E91DC51A0D009CE2B4 /* Build configuration list for PBXNativeTarget "IGListKit-tvOS" */; buildPhases = ( F15F1DE12BA40D15002AF3FE /* Copy Privacy Manifest */, 885FE1CF1DC51A0D009CE2B4 /* Sources */, 885FE1D01DC51A0D009CE2B4 /* Frameworks */, 885FE1D11DC51A0D009CE2B4 /* Headers */, ); buildRules = ( ); dependencies = ( 7A02D0BD23615B8B00B49FAE /* PBXTargetDependency */, ); name = "IGListKit-tvOS"; productName = "IGListKit-tvOS"; productReference = 885FE1D41DC51A0D009CE2B4 /* IGListKit.framework */; productType = "com.apple.product-type.framework"; }; 885FE1DB1DC51A0D009CE2B4 /* IGListKit-tvOSTests */ = { isa = PBXNativeTarget; buildConfigurationList = 885FE1EA1DC51A0D009CE2B4 /* Build configuration list for PBXNativeTarget "IGListKit-tvOSTests" */; buildPhases = ( 885FE1D81DC51A0D009CE2B4 /* Sources */, 885FE1D91DC51A0D009CE2B4 /* Frameworks */, 885FE1DA1DC51A0D009CE2B4 /* Resources */, ); buildRules = ( ); dependencies = ( 885FE1DF1DC51A0D009CE2B4 /* PBXTargetDependency */, ); name = "IGListKit-tvOSTests"; productName = "IGListKit-tvOSTests"; productReference = 885FE1DC1DC51A0D009CE2B4 /* IGListKit-tvOSTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 887D0B351D870D7E009E01F7 /* IGListKit */ = { isa = PBXNativeTarget; buildConfigurationList = 887D0B4A1D870D7F009E01F7 /* Build configuration list for PBXNativeTarget "IGListKit" */; buildPhases = ( F15F1DDF2BA40CF4002AF3FE /* Copy Privacy Manifest */, 887D0B311D870D7E009E01F7 /* Sources */, 887D0B321D870D7E009E01F7 /* Frameworks */, 887D0B331D870D7E009E01F7 /* Headers */, ); buildRules = ( ); dependencies = ( 7A02D0B32361546300B49FAE /* PBXTargetDependency */, ); name = IGListKit; productName = IGListKit; productReference = 887D0B361D870D7E009E01F7 /* IGListKit.framework */; productType = "com.apple.product-type.framework"; }; 887D0B3E1D870D7F009E01F7 /* IGListKitTests */ = { isa = PBXNativeTarget; buildConfigurationList = 887D0B4D1D870D7F009E01F7 /* Build configuration list for PBXNativeTarget "IGListKitTests" */; buildPhases = ( 887D0B3B1D870D7F009E01F7 /* Sources */, 887D0B3C1D870D7F009E01F7 /* Frameworks */, 887D0B3D1D870D7F009E01F7 /* Resources */, ); buildRules = ( ); dependencies = ( 887D0B421D870D7F009E01F7 /* PBXTargetDependency */, ); name = IGListKitTests; productName = IGListKitTests; productReference = 887D0B3F1D870D7F009E01F7 /* IGListKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 88DF897B1E010E6A00B1B9B4 /* IGListKit-macOSTests */ = { isa = PBXNativeTarget; buildConfigurationList = 88DF89861E010E6A00B1B9B4 /* Build configuration list for PBXNativeTarget "IGListKit-macOSTests" */; buildPhases = ( 88DF89781E010E6A00B1B9B4 /* Sources */, 88DF89791E010E6A00B1B9B4 /* Frameworks */, 88DF897A1E010E6A00B1B9B4 /* Resources */, ); buildRules = ( ); dependencies = ( 88DF89831E010E6A00B1B9B4 /* PBXTargetDependency */, ); name = "IGListKit-macOSTests"; productName = "IGListKitTests-macOS"; productReference = 88DF897C1E010E6A00B1B9B4 /* IGListKit-macOSTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; DD3152661DE227FA00AC9D2C /* IGListKit-macOS */ = { isa = PBXNativeTarget; buildConfigurationList = DD3152A91DE227FA00AC9D2C /* Build configuration list for PBXNativeTarget "IGListKit-macOS" */; buildPhases = ( F15F1DE32BA40D27002AF3FE /* Copy Privacy Manifest */, DD3152671DE227FA00AC9D2C /* Sources */, DD31527B1DE227FA00AC9D2C /* Frameworks */, DD31527C1DE227FA00AC9D2C /* Headers */, ); buildRules = ( ); dependencies = ( 7A02D0BF23615B9000B49FAE /* PBXTargetDependency */, ); name = "IGListKit-macOS"; productName = IGListKit; productReference = DD3152AC1DE227FA00AC9D2C /* IGListKit.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 887D0B2D1D870D7E009E01F7 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; CLASSPREFIX = IG; LastSwiftUpdateCheck = 1120; LastUpgradeCheck = 2620; ORGANIZATIONNAME = Instagram; TargetAttributes = { 7A02D01C2361520200B49FAE = { CreatedOnToolsVersion = 11.1; ProvisioningStyle = Manual; }; 7A02D02A2361525800B49FAE = { CreatedOnToolsVersion = 11.1; ProvisioningStyle = Manual; }; 7A02D0372361526600B49FAE = { CreatedOnToolsVersion = 11.1; ProvisioningStyle = Manual; }; 7A92EA9623A8A42000E5A13D = { CreatedOnToolsVersion = 11.2.1; LastSwiftMigration = 1620; ProvisioningStyle = Automatic; }; 7A92EAA723A8A50100E5A13D = { CreatedOnToolsVersion = 11.2.1; LastSwiftMigration = 1620; ProvisioningStyle = Automatic; }; 885FE1D31DC51A0D009CE2B4 = { CreatedOnToolsVersion = 8.1; ProvisioningStyle = Manual; }; 885FE1DB1DC51A0D009CE2B4 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 0810; ProvisioningStyle = Manual; }; 887D0B351D870D7E009E01F7 = { CreatedOnToolsVersion = 8.0; LastSwiftMigration = 0800; ProvisioningStyle = Manual; }; 887D0B3E1D870D7F009E01F7 = { CreatedOnToolsVersion = 8.0; LastSwiftMigration = 1120; ProvisioningStyle = Manual; }; 88DF897B1E010E6A00B1B9B4 = { CreatedOnToolsVersion = 8.2; LastSwiftMigration = 0820; ProvisioningStyle = Manual; }; DD3152661DE227FA00AC9D2C = { ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 887D0B301D870D7E009E01F7 /* Build configuration list for PBXProject "IGListKit" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 887D0B2C1D870D7E009E01F7; packageReferences = ( F12D8EA82E93852600A7DDBE /* XCRemoteSwiftPackageReference "ocmock" */, ); productRefGroup = 887D0B371D870D7E009E01F7 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 887D0B351D870D7E009E01F7 /* IGListKit */, 887D0B3E1D870D7F009E01F7 /* IGListKitTests */, 885FE1D31DC51A0D009CE2B4 /* IGListKit-tvOS */, 885FE1DB1DC51A0D009CE2B4 /* IGListKit-tvOSTests */, DD3152661DE227FA00AC9D2C /* IGListKit-macOS */, 88DF897B1E010E6A00B1B9B4 /* IGListKit-macOSTests */, 7A02D01C2361520200B49FAE /* IGListDiffKit */, 7A02D02A2361525800B49FAE /* IGListDiffKit-tvOS */, 7A02D0372361526600B49FAE /* IGListDiffKit-macOS */, 7A92EA9623A8A42000E5A13D /* IGListSwiftKit */, 7A92EAA723A8A50100E5A13D /* IGListSwiftKit-tvOS */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 7A92EA9523A8A42000E5A13D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EAA623A8A50100E5A13D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1DA1DC51A0D009CE2B4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B3D1D870D7F009E01F7 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 2914BEE91DCD15F400C96401 /* IGTestNibSupplementaryView.xib in Resources */, 29EA6C491DB43A8000957A88 /* IGTestNibCell.xib in Resources */, 821BC4C41DB8CEF800172ED0 /* IGTestStoryboard.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 88DF897A1E010E6A00B1B9B4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ F105D9AE29B7193000B7F361 /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = SwiftLint; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "scripts/lint.sh\n"; }; F105D9AF29B7194300B7F361 /* SwiftLint */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = SwiftLint; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "scripts/lint.sh\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 7A02D0192361520200B49FAE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7A02D06D2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */, 7A02D0912361529F00B49FAE /* IGListMoveIndex.m in Sources */, 7A02D08E2361529F00B49FAE /* IGListIndexSetResult.m in Sources */, 7A02D09D2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */, 7A02D0852361529F00B49FAE /* IGListDiff.mm in Sources */, 7A02D0972361529F00B49FAE /* IGListMoveIndexPath.m in Sources */, 7A02D08B2361529F00B49FAE /* IGListIndexPathResult.m in Sources */, 7A02D05E2361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0272361525800B49FAE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7A02D06E2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */, 7A02D0922361529F00B49FAE /* IGListMoveIndex.m in Sources */, 7A02D08F2361529F00B49FAE /* IGListIndexSetResult.m in Sources */, 7A02D09E2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */, 7A02D0862361529F00B49FAE /* IGListDiff.mm in Sources */, 7A02D0982361529F00B49FAE /* IGListMoveIndexPath.m in Sources */, 7A02D08C2361529F00B49FAE /* IGListIndexPathResult.m in Sources */, 7A02D05F2361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A02D0342361526600B49FAE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7A02D06F2361529F00B49FAE /* NSString+IGListDiffable.m in Sources */, 7A02D0932361529F00B49FAE /* IGListMoveIndex.m in Sources */, 7A02D0902361529F00B49FAE /* IGListIndexSetResult.m in Sources */, 7A02D09F2361529F00B49FAE /* IGListBatchUpdateData.mm in Sources */, 7A02D0872361529F00B49FAE /* IGListDiff.mm in Sources */, 7A02D0992361529F00B49FAE /* IGListMoveIndexPath.m in Sources */, 7A02D08D2361529F00B49FAE /* IGListIndexPathResult.m in Sources */, 7A02D0602361529F00B49FAE /* NSNumber+IGListDiffable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EA9323A8A42000E5A13D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F15DB2F62D813B090066C7AD /* IGListAdapter+Async.swift in Sources */, F15DB2F72D813B090066C7AD /* ListValueSectionController.swift in Sources */, F15DB2F82D813B090066C7AD /* IGListSingleSectionController+Refinements.swift in Sources */, F15DB2F92D813B090066C7AD /* IGListCollectionContext+Refinements.swift in Sources */, F15DB2FA2D813B090066C7AD /* ListIdentifiable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 7A92EAA423A8A50100E5A13D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F15DB2F12D813B090066C7AD /* IGListAdapter+Async.swift in Sources */, F15DB2F22D813B090066C7AD /* ListValueSectionController.swift in Sources */, F15DB2F32D813B090066C7AD /* IGListSingleSectionController+Refinements.swift in Sources */, F15DB2F42D813B090066C7AD /* IGListCollectionContext+Refinements.swift in Sources */, F15DB2F52D813B090066C7AD /* ListIdentifiable.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1CF1DC51A0D009CE2B4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 0A8928F926CDA521003FABD8 /* IGListUpdateTransactionBuilder.m in Sources */, 0A8928FF26CDA62C003FABD8 /* IGListBatchUpdateTransaction.m in Sources */, 576029E92C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */, 0A89290226CDA672003FABD8 /* IGListItemUpdatesCollector.m in Sources */, 7A02CF492361511100B49FAE /* IGListSingleSectionController.m in Sources */, 0A8928FC26CDA5BD003FABD8 /* IGListReloadTransaction.m in Sources */, 7A02CF132361511100B49FAE /* IGListGenericSectionController.m in Sources */, 7A02CFA62361513600B49FAE /* UICollectionView+IGListBatchUpdateData.m in Sources */, 7A02CFC42361513600B49FAE /* UICollectionView+DebugDescription.m in Sources */, 0A8928FD26CDA5E1003FABD8 /* IGListDataSourceChangeTransaction.m in Sources */, 7A02CFBE2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.m in Sources */, 7A02CFAC2361513600B49FAE /* IGListReloadIndexPath.m in Sources */, 7A02CF5E2361511100B49FAE /* IGListSectionController.m in Sources */, 7A02D00C2361513600B49FAE /* IGListSectionMap+DebugDescription.m in Sources */, A46A1D3B2D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m in Sources */, 7A02CF612361511100B49FAE /* IGListCollectionView.m in Sources */, 7A02CFBB2361513600B49FAE /* IGListAdapterProxy.m in Sources */, 576029E52C61B91D006E50E2 /* IGListViewVisibilityTracker.m in Sources */, 7A02CFF42361513600B49FAE /* IGListAdapter+UICollectionView.m in Sources */, 7A02CF3A2361511100B49FAE /* IGListCollectionViewLayout.mm in Sources */, 7A02CF2B2361511100B49FAE /* IGListReloadDataUpdater.m in Sources */, 576029E12C61B91D006E50E2 /* IGListPerformDiff.m in Sources */, 7A02CFF12361513600B49FAE /* IGListBindingSectionController+DebugDescription.m in Sources */, F18CC76D29EFBD0300DC3B9A /* IGListBindingSingleSectionController.m in Sources */, 7A02CFFA2361513600B49FAE /* IGListWorkingRangeHandler.mm in Sources */, E03DEA8F255C9AB200ACCAFC /* IGListTransitionData.m in Sources */, 7A02CFB22361513600B49FAE /* UIScrollView+IGListKit.m in Sources */, 7A02CF582361511100B49FAE /* IGListBindingSectionController.m in Sources */, 7A02CFE52361513600B49FAE /* IGListDisplayHandler.m in Sources */, 7A02CFC72361513600B49FAE /* IGListSectionMap.m in Sources */, 7A02D0002361513600B49FAE /* IGListDebugger.m in Sources */, 7A02CF342361511100B49FAE /* IGListAdapterUpdater.m in Sources */, A46A1D2B2D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m in Sources */, A46A1D2C2D80213D00CB9157 /* UIViewController+IGListAdapter.m in Sources */, 7A02D0062361513600B49FAE /* IGListDebuggingUtilities.m in Sources */, 7A02CF522361511100B49FAE /* IGListAdapter.m in Sources */, 7A02CF942361513600B49FAE /* IGListAdapter+DebugDescription.m in Sources */, 7A92EAB523A8A5FF00E5A13D /* IGListAdapterUpdaterHelpers.m in Sources */, 7A02CFD92361513600B49FAE /* IGListBatchUpdateData+DebugDescription.m in Sources */, 7A02CFAF2361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.m in Sources */, F102DBBC2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 885FE1D81DC51A0D009CE2B4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 298DDA381E3B168E00F76F50 /* IGLayoutTestItem.m in Sources */, 885FE2311DC51B76009CE2B4 /* IGListDisplayHandlerTests.m in Sources */, 298DDA3B1E3B16F800F76F50 /* IGLayoutTestDataSource.m in Sources */, 29C474901DDF460500AE68CE /* IGListSectionMapTests.m in Sources */, 29C579321DE0DA8A003A149B /* IGTestStoryboardSupplementarySource.m in Sources */, 298DDA131E3AE3F100F76F50 /* IGTestDiffingDataSource.m in Sources */, F1ED68B329E9B3B9003744F8 /* IGListInteractiveMovingTests.m in Sources */, F18CC76029EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m in Sources */, F18CC76729EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m in Sources */, 29C5792F1DE0DA8A003A149B /* IGListTestAdapterStoryboardDataSource.m in Sources */, 885FE23B1DC51B86009CE2B4 /* IGListTestUICollectionViewDataSource.m in Sources */, 885FE23D1DC51B86009CE2B4 /* IGTestDelegateController.m in Sources */, 298DDA251E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m in Sources */, 885FE22B1DC51B76009CE2B4 /* IGListAdapterE2ETests.m in Sources */, 885FE2331DC51B76009CE2B4 /* IGListSingleSectionControllerTests.m in Sources */, 13DF01741FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m in Sources */, E56B7B3520A9D7100071010C /* IGListCollectionScrollingTraitsTests.m in Sources */, 29C579311DE0DA8A003A149B /* IGTestNibSupplementaryView.m in Sources */, 13DF01781FA1000E0092A320 /* IGTestReorderableSection.m in Sources */, 885FE23C1DC51B86009CE2B4 /* IGTestCell.m in Sources */, 298DDA001E3AE28000F76F50 /* IGTestDiffingObject.m in Sources */, 29C579331DE0DA8A003A149B /* IGTestStoryboardSupplementaryView.m in Sources */, 16B71CF022B0A08400FE96ED /* IGTestInvalidateLayoutObject.m in Sources */, F1ED68BB29E9B40A003744F8 /* IGListGenericSectionControllerTests.m in Sources */, 16B71CEC22B0A08400FE96ED /* IGTestInvalidateLayoutSectionController.m in Sources */, 2995409F1F588C9500F647CF /* IGTestBindingWithoutDeselectionDelegate.m in Sources */, 885FE2401DC51B86009CE2B4 /* IGTestSingleItemDataSource.m in Sources */, 885FE2451DC51B86009CE2B4 /* IGTestStoryboardCell.m in Sources */, F1E6302429EA43080060B4D6 /* IGListSectionControllerTests.m in Sources */, 16B71CEE22B0A08400FE96ED /* IGTestInvalidateLayoutDataSource.m in Sources */, F10C8F5C2B9830F1009F4690 /* IGListTestCollectionViewLayout.m in Sources */, 298DD9CF1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m in Sources */, 885FE22F1DC51B76009CE2B4 /* IGListDiffSwiftTests.swift in Sources */, 885FE23F1DC51B86009CE2B4 /* IGTestObject.m in Sources */, F1855A4D29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */, 828540511DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m in Sources */, 885FE2371DC51B76009CE2B4 /* IGListWorkingRangeHandlerTests.m in Sources */, 8285404D1DE40C6E00118B94 /* IGListTestHorizontalSection.m in Sources */, 22907ACA2F2870020015F3D0 /* IGListPerformDiffTests.m in Sources */, 298DDA061E3AE2B000F76F50 /* IGTestNumberBindableCell.m in Sources */, 885FE2381DC51B86009CE2B4 /* IGListTestAdapterDataSource.m in Sources */, F1ED68B529E9B3B9003744F8 /* IGListDebugDescriptionTests.m in Sources */, 885FE2341DC51B76009CE2B4 /* IGListSingleNibItemControllerTests.m in Sources */, 885FE2391DC51B86009CE2B4 /* IGListTestOffsettingLayout.m in Sources */, 29C4748F1DDF460500AE68CE /* IGListDiffResultTests.m in Sources */, F1ED68B729E9B3B9003744F8 /* IGListTransactionTests.m in Sources */, F1ED68BE29E9B41A003744F8 /* IGListContentInsetTests.m in Sources */, 5766613F2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */, 22907ABD2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m in Sources */, 885FE2421DC51B86009CE2B4 /* IGTestSingleStoryboardItemDataSource.m in Sources */, 885FE2301DC51B76009CE2B4 /* IGListDiffTests.m in Sources */, 885FE22E1DC51B76009CE2B4 /* IGListBatchUpdateDataTests.m in Sources */, 22907AC72F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m in Sources */, 298DDA3C1E3B170300F76F50 /* IGLayoutTestSection.m in Sources */, 298DDA0A1E3AE31E00F76F50 /* IGTestDiffingSectionController.m in Sources */, 29C4748D1DDF45F900AE68CE /* IGListAdapterProxyTests.m in Sources */, 82914C5C1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */, 88379729202236AC00B94676 /* IGListAdapterUpdateTester.m in Sources */, 29DA5CA81EA7D37000113926 /* IGListTestCase.m in Sources */, F18CC76429EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m in Sources */, F18CC75D29EFBB9400DC3B9A /* IGListCollectionViewTests.m in Sources */, 885FE22C1DC51B76009CE2B4 /* IGListAdapterTests.m in Sources */, 298DDA051E3AE2B000F76F50 /* IGTestStringBindableCell.m in Sources */, 885FE22D1DC51B76009CE2B4 /* IGListAdapterUpdaterTests.m in Sources */, 885FE2351DC51B76009CE2B4 /* IGListSingleStoryboardItemControllerTests.m in Sources */, 29C4748E1DDF460500AE68CE /* IGListAdapterStoryboardTests.m in Sources */, 885FE2411DC51B86009CE2B4 /* IGTestSingleNibItemDataSource.m in Sources */, 6A9EB3621F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m in Sources */, 885FE2461DC51B86009CE2B4 /* IGTestSupplementarySource.m in Sources */, 885FE2431DC51B86009CE2B4 /* IGTestStoryboardViewController.m in Sources */, F1ED68BC29E9B411003744F8 /* IGListDebuggerTests.m in Sources */, 885FE23A1DC51B86009CE2B4 /* IGListTestSection.m in Sources */, 22907AC52F2866160015F3D0 /* IGListUpdateCoalescerTests.m in Sources */, 29C579301DE0DA8A003A149B /* IGListTestStoryboardSection.m in Sources */, 22907AC22F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m in Sources */, 885FE23E1DC51B86009CE2B4 /* IGTestDelegateDataSource.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B311D870D7E009E01F7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7A02CF482361511100B49FAE /* IGListSingleSectionController.m in Sources */, 57B22E872502AAC40055DC2F /* IGListDataSourceChangeTransaction.m in Sources */, 576029E82C61B91D006E50E2 /* IGListUpdateCoalescer.m in Sources */, 7A02CF122361511100B49FAE /* IGListGenericSectionController.m in Sources */, 7A02CFA52361513600B49FAE /* UICollectionView+IGListBatchUpdateData.m in Sources */, 57B22E832502AAC40055DC2F /* IGListItemUpdatesCollector.m in Sources */, 7A02CFC32361513600B49FAE /* UICollectionView+DebugDescription.m in Sources */, 7A02CFBD2361513600B49FAE /* IGListAdapterUpdater+DebugDescription.m in Sources */, 7A02CFAB2361513600B49FAE /* IGListReloadIndexPath.m in Sources */, 7A02CF5D2361511100B49FAE /* IGListSectionController.m in Sources */, 7A02D00B2361513600B49FAE /* IGListSectionMap+DebugDescription.m in Sources */, 7A02CF602361511100B49FAE /* IGListCollectionView.m in Sources */, 7A02CFBA2361513600B49FAE /* IGListAdapterProxy.m in Sources */, 57B22E8C2502AAC40055DC2F /* IGListReloadTransaction.m in Sources */, A46A1D392D80222F00CB9157 /* IGListCollectionViewLayoutInvalidationContext.m in Sources */, 7A02CFF32361513600B49FAE /* IGListAdapter+UICollectionView.m in Sources */, 7A02CF392361511100B49FAE /* IGListCollectionViewLayout.mm in Sources */, 576029E42C61B91D006E50E2 /* IGListViewVisibilityTracker.m in Sources */, 57B22E7F2502AAC40055DC2F /* IGListBatchUpdateTransaction.m in Sources */, 57B22E802502AAC40055DC2F /* IGListUpdateTransactionBuilder.m in Sources */, 7A02CF2A2361511100B49FAE /* IGListReloadDataUpdater.m in Sources */, 576029E02C61B91D006E50E2 /* IGListPerformDiff.m in Sources */, 7A02CFF02361513600B49FAE /* IGListBindingSectionController+DebugDescription.m in Sources */, F18CC76C29EFBD0300DC3B9A /* IGListBindingSingleSectionController.m in Sources */, 7A02CFF92361513600B49FAE /* IGListWorkingRangeHandler.mm in Sources */, 57B22E6C2502AAB20055DC2F /* IGListTransitionData.m in Sources */, 7A02CFB12361513600B49FAE /* UIScrollView+IGListKit.m in Sources */, 7A02CF572361511100B49FAE /* IGListBindingSectionController.m in Sources */, 7A02CFE42361513600B49FAE /* IGListDisplayHandler.m in Sources */, 7A02CFC62361513600B49FAE /* IGListSectionMap.m in Sources */, 7A02CFFF2361513600B49FAE /* IGListDebugger.m in Sources */, 7A02CF332361511100B49FAE /* IGListAdapterUpdater.m in Sources */, A46A1D312D80213D00CB9157 /* IGListAdapterDelegateAnnouncer.m in Sources */, A46A1D322D80213D00CB9157 /* UIViewController+IGListAdapter.m in Sources */, 7A02D0052361513600B49FAE /* IGListDebuggingUtilities.m in Sources */, 7A02CF512361511100B49FAE /* IGListAdapter.m in Sources */, 7A02CF932361513600B49FAE /* IGListAdapter+DebugDescription.m in Sources */, 7AD6864B23A89E7F009000DE /* IGListAdapterUpdaterHelpers.m in Sources */, 7A02CFD82361513600B49FAE /* IGListBatchUpdateData+DebugDescription.m in Sources */, 7A02CFAE2361513600B49FAE /* UICollectionViewLayout+InteractiveReordering.m in Sources */, F102DBBB2ACE4CD500394DB7 /* IGListArrayUtilsInternal.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 887D0B3B1D870D7F009E01F7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 298DDA391E3B168F00F76F50 /* IGLayoutTestItem.m in Sources */, 13DF01731FA0FD400092A320 /* IGListTestAdapterReorderingDataSource.m in Sources */, 88144F181D870EDC007C7F66 /* IGTestDelegateController.m in Sources */, 290DF3771E9323E6009FE456 /* IGListDebuggerTests.m in Sources */, 298DDA3A1E3B16F600F76F50 /* IGLayoutTestDataSource.m in Sources */, 88144F0D1D870EDC007C7F66 /* IGListDisplayHandlerTests.m in Sources */, 298DDA141E3AE3F300F76F50 /* IGTestDiffingDataSource.m in Sources */, 8240C7F51DC2D99300B3AAE7 /* IGTestStoryboardSupplementarySource.m in Sources */, 88144F1B1D870EDC007C7F66 /* IGTestSingleItemDataSource.m in Sources */, F1E6302329EA43080060B4D6 /* IGListSectionControllerTests.m in Sources */, 88144F171D870EDC007C7F66 /* IGTestCell.m in Sources */, 298DDA241E3B15EE00F76F50 /* IGListCollectionViewLayoutTests.m in Sources */, 821BC4C01DB8C9D500172ED0 /* IGListSingleStoryboardItemControllerTests.m in Sources */, 294AC6321DDE4C19002FCE5D /* IGListDiffResultTests.m in Sources */, 88144F141D870EDC007C7F66 /* IGListTestOffsettingLayout.m in Sources */, 8240C7FB1DC2F6CF00B3AAE7 /* IGListTestAdapterStoryboardDataSource.m in Sources */, 298DDA011E3AE28000F76F50 /* IGTestDiffingObject.m in Sources */, 88144F131D870EDC007C7F66 /* IGListTestAdapterDataSource.m in Sources */, 88144F071D870EDC007C7F66 /* IGListAdapterE2ETests.m in Sources */, F1ED68BA29E9B404003744F8 /* IGListInteractiveMovingTests.m in Sources */, 88144F1A1D870EDC007C7F66 /* IGTestObject.m in Sources */, F1ED68BF29E9B443003744F8 /* IGListTransactionTests.m in Sources */, 298DD9CE1E3ADD1400F76F50 /* IGListBindingSectionControllerTests.m in Sources */, 88144F0B1D870EDC007C7F66 /* IGListDiffSwiftTests.swift in Sources */, F18CC75F29EFBBB300DC3B9A /* IGListBindingSingleSectionControllerTests.m in Sources */, 88144F191D870EDC007C7F66 /* IGTestDelegateDataSource.m in Sources */, 828540501DE40D2D00118B94 /* IGListTestAdapterHorizontalDataSource.m in Sources */, 88144F0C1D870EDC007C7F66 /* IGListDiffTests.m in Sources */, 8285404C1DE40C6E00118B94 /* IGListTestHorizontalSection.m in Sources */, 298DDA081E3AE2B100F76F50 /* IGTestNumberBindableCell.m in Sources */, F10C8F5B2B9830F0009F4690 /* IGListTestCollectionViewLayout.m in Sources */, 2995409E1F588C9400F647CF /* IGTestBindingWithoutDeselectionDelegate.m in Sources */, 88144F0A1D870EDC007C7F66 /* IGListBatchUpdateDataTests.m in Sources */, 26271C8C1DAE96740073E116 /* IGListSingleNibItemControllerTests.m in Sources */, 29DA5CA31EA7C72400113926 /* IGListGenericSectionControllerTests.m in Sources */, 13DF01771FA1000E0092A320 /* IGTestReorderableSection.m in Sources */, 829D7BAA1DD1819000549816 /* IGListSectionMapTests.m in Sources */, F1ED68BD29E9B415003744F8 /* IGListDebugDescriptionTests.m in Sources */, 22907ACB2F2870020015F3D0 /* IGListPerformDiffTests.m in Sources */, E56B7B3420A9D7100071010C /* IGListCollectionScrollingTraitsTests.m in Sources */, 16B71CEB22B0A08400FE96ED /* IGTestInvalidateLayoutSectionController.m in Sources */, 29C5792E1DE0DA89003A149B /* IGTestNibSupplementaryView.m in Sources */, 88144F101D870EDC007C7F66 /* IGListSingleSectionControllerTests.m in Sources */, 88144F121D870EDC007C7F66 /* IGListWorkingRangeHandlerTests.m in Sources */, F1855A4C29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */, 821BC4D31DB981AB00172ED0 /* IGTestSingleStoryboardItemDataSource.m in Sources */, 298DDA3D1E3B170400F76F50 /* IGLayoutTestSection.m in Sources */, 5766613E2CB5A72500E20F73 /* IGListAdapterDelegateAnnouncerTests.m in Sources */, 22907ABE2F2862830015F3D0 /* IGListViewVisibilityTrackerTests.m in Sources */, 298DDA091E3AE31D00F76F50 /* IGTestDiffingSectionController.m in Sources */, 88144F151D870EDC007C7F66 /* IGListTestSection.m in Sources */, 82914C5B1E6E2DEC0066C2F8 /* IGListTestContainerSizeSection.m in Sources */, 22907AC82F28679B0015F3D0 /* UIViewControllerIGListAdapterTests.m in Sources */, 29DA5CA71EA7D37000113926 /* IGListTestCase.m in Sources */, 88144F1D1D870EDC007C7F66 /* IGTestSupplementarySource.m in Sources */, F18CC76629EFBC3800DC3B9A /* IGListReloadDataUpdaterTests.m in Sources */, E8D312E01FC472A60009FA2F /* IGListContentInsetTests.m in Sources */, 298DDA071E3AE2B100F76F50 /* IGTestStringBindableCell.m in Sources */, 16B71CED22B0A08400FE96ED /* IGTestInvalidateLayoutDataSource.m in Sources */, 16B71CEF22B0A08400FE96ED /* IGTestInvalidateLayoutObject.m in Sources */, 88144F081D870EDC007C7F66 /* IGListAdapterTests.m in Sources */, 8240C7F21DC284C300B3AAE7 /* IGListAdapterStoryboardTests.m in Sources */, 8240C7F01DC272CA00B3AAE7 /* IGTestStoryboardSupplementaryView.m in Sources */, 88379728202236AB00B94676 /* IGListAdapterUpdateTester.m in Sources */, 821BC4CB1DB8D60100172ED0 /* IGTestStoryboardViewController.m in Sources */, 821BC4D01DB8D90900172ED0 /* IGTestStoryboardCell.m in Sources */, 88144F161D870EDC007C7F66 /* IGListTestUICollectionViewDataSource.m in Sources */, 88144F091D870EDC007C7F66 /* IGListAdapterUpdaterTests.m in Sources */, 29C4748C1DDF45F400AE68CE /* IGListAdapterProxyTests.m in Sources */, F18CC75C29EFBB9400DC3B9A /* IGListCollectionViewTests.m in Sources */, 8240C7F81DC2F3FB00B3AAE7 /* IGListTestStoryboardSection.m in Sources */, F18CC76329EFBBE200DC3B9A /* IGTestBindingSingleItemDataSource.m in Sources */, 22907AC42F2866160015F3D0 /* IGListUpdateCoalescerTests.m in Sources */, 26271C8A1DAE94E40073E116 /* IGTestSingleNibItemDataSource.m in Sources */, 22907AC12F2864450015F3D0 /* IGListItemUpdatesCollectorTests.m in Sources */, 6A9EB3611F841E5D0070C572 /* IGTestSingleWithoutDeselectionDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 88DF89781E010E6A00B1B9B4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 88DF898A1E010F7000B1B9B4 /* IGListDiffTests.m in Sources */, 88DF89891E010F6500B1B9B4 /* IGListDiffSwiftTests.swift in Sources */, 882BC1321E0118CB0083B311 /* IGTestObject.m in Sources */, 0B3B93611E08E38C008390ED /* IGListBatchUpdateDataTests.m in Sources */, F1855A4E29BC565600558D18 /* IGListDiffDescriptionStringTests.m in Sources */, 88DF89881E010F5C00B1B9B4 /* IGListDiffResultTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; DD3152671DE227FA00AC9D2C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 7A02D0B32361546300B49FAE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7A02D01C2361520200B49FAE /* IGListDiffKit */; targetProxy = 7A02D0B22361546300B49FAE /* PBXContainerItemProxy */; }; 7A02D0BD23615B8B00B49FAE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7A02D02A2361525800B49FAE /* IGListDiffKit-tvOS */; targetProxy = 7A02D0BC23615B8B00B49FAE /* PBXContainerItemProxy */; }; 7A02D0BF23615B9000B49FAE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7A02D0372361526600B49FAE /* IGListDiffKit-macOS */; targetProxy = 7A02D0BE23615B9000B49FAE /* PBXContainerItemProxy */; }; 7A92EAA223A8A4E700E5A13D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 887D0B351D870D7E009E01F7 /* IGListKit */; targetProxy = 7A92EAA123A8A4E700E5A13D /* PBXContainerItemProxy */; }; 7A92EAB323A8A53F00E5A13D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 885FE1D31DC51A0D009CE2B4 /* IGListKit-tvOS */; targetProxy = 7A92EAB223A8A53F00E5A13D /* PBXContainerItemProxy */; }; 885FE1DF1DC51A0D009CE2B4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 885FE1D31DC51A0D009CE2B4 /* IGListKit-tvOS */; targetProxy = 885FE1DE1DC51A0D009CE2B4 /* PBXContainerItemProxy */; }; 887D0B421D870D7F009E01F7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 887D0B351D870D7E009E01F7 /* IGListKit */; targetProxy = 887D0B411D870D7F009E01F7 /* PBXContainerItemProxy */; }; 88DF89831E010E6A00B1B9B4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DD3152661DE227FA00AC9D2C /* IGListKit-macOS */; targetProxy = 88DF89821E010E6A00B1B9B4 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 7A02D0232361520200B49FAE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 7A02D0242361520200B49FAE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 7A02D0312361525800B49FAE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = IGListDiffKit; SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 11.0; }; name = Debug; }; 7A02D0322361525800B49FAE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = IGListDiffKit; SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 11.0; }; name = Release; }; 7A02D03E2361526600B49FAE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = IGListDiffKit; SDKROOT = macosx; SKIP_INSTALL = YES; }; name = Debug; }; 7A02D03F2361526600B49FAE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListDiffKit; PRODUCT_NAME = IGListDiffKit; SDKROOT = macosx; SKIP_INSTALL = YES; }; name = Release; }; 7A92EA9D23A8A42000E5A13D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListSwiftKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 7A92EA9E23A8A42000E5A13D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListSwiftKit; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 7A92EAAE23A8A50100E5A13D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListSwiftKit-tvOS"; PRODUCT_NAME = IGListSwiftKit; SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 13.2; }; name = Debug; }; 7A92EAAF23A8A50100E5A13D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListSwiftKit-tvOS"; PRODUCT_NAME = IGListSwiftKit; SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 13.2; }; name = Release; }; 885FE1E51DC51A0D009CE2B4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = IGListKit; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 11.0; }; name = Debug; }; 885FE1E61DC51A0D009CE2B4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = IGListKit; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 11.0; }; name = Release; }; 885FE1E71DC51A0D009CE2B4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", "NS_BLOCK_ASSERTIONS=1", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; }; name = Debug; }; 885FE1E81DC51A0D009CE2B4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = appletvos; SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; }; name = Release; }; 887D0B481D870D7F009E01F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_CODE_COVERAGE = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = 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 = 12.0; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-fmodules"; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-fcxx-modules", ); SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 12.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 887D0B491D870D7F009E01F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_CODE_COVERAGE = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = 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 = 12.0; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = "-fmodules"; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-fcxx-modules", ); SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 12.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 887D0B4B1D870D7F009E01F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_SHADOW = YES; GCC_WARN_UNUSED_PARAMETER = NO; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_CFLAGS = ( "-Wambiguous-macro", "-Wbool-conversion", "-Wcomment", "-Wconditional-uninitialized", "-Wconstant-logical-operand", "-Wcovered-switch-default", "-Wdangling-else", "-Wdeprecated-increment-bool", "-Wdivision-by-zero", "-Wduplicate-enum", "-Wempty-body", "-Wenum-compare", "-Wenum-conversion", "-Wformat", "-Wformat-extra-args", "-Wformat-security", "-Wformat-zero-length", "-Wignored-attributes", "-Wignored-attributes", "-Wimplicit-atomic-properties", "-Wimplicit-fallthrough", "-Wimplicit-retain-self", "-Wincomplete-implementation", "-Wloop-analysis", "-Wmismatched-return-types", "-Wnewline-eof", "-Wnonnull", "-Wnull-character", "-Wobjc-method-access", "-Wobjc-missing-super-calls", "-Wreturn-type", "-Wself-assign", "-Wswitch", "-Wswitch-bool", "-Wswitch-enum", "-Wunicode", "-Wunknown-pragmas", "-Wunused-const-variable", "-Wunused-function", "-Wunused-label", "-Wunused-property-ivar", "-Wunused-result", "-Wunused-value", "-Wnullable-to-nonnull-conversion", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSiON = 5.0; }; name = Debug; }; 887D0B4C1D870D7F009E01F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_SHADOW = YES; GCC_WARN_UNUSED_PARAMETER = NO; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_CFLAGS = ( "-Wambiguous-macro", "-Wbool-conversion", "-Wcomment", "-Wconditional-uninitialized", "-Wconstant-logical-operand", "-Wcovered-switch-default", "-Wdangling-else", "-Wdeprecated-increment-bool", "-Wdivision-by-zero", "-Wduplicate-enum", "-Wempty-body", "-Wenum-compare", "-Wenum-conversion", "-Wformat", "-Wformat-extra-args", "-Wformat-security", "-Wformat-zero-length", "-Wignored-attributes", "-Wignored-attributes", "-Wimplicit-atomic-properties", "-Wimplicit-fallthrough", "-Wimplicit-retain-self", "-Wincomplete-implementation", "-Wloop-analysis", "-Wmismatched-return-types", "-Wnewline-eof", "-Wnonnull", "-Wnull-character", "-Wobjc-method-access", "-Wobjc-missing-super-calls", "-Wreturn-type", "-Wself-assign", "-Wswitch", "-Wswitch-bool", "-Wswitch-enum", "-Wunicode", "-Wunknown-pragmas", "-Wunused-const-variable", "-Wunused-function", "-Wunused-label", "-Wunused-property-ivar", "-Wunused-result", "-Wunused-value", "-Wnullable-to-nonnull-conversion", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSiON = 5.0; }; name = Release; }; 887D0B4E1D870D7F009E01F7 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", "NS_BLOCK_ASSERTIONS=1", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; 887D0B4F1D870D7F009E01F7 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Tests/IGListKitTests-Bridging-Header.h"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; 88DF89841E010E6A00B1B9B4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", "NS_BLOCK_ASSERTIONS=1", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListKitTests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; 88DF89851E010E6A00B1B9B4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_SHADOW = YES; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.5; PRODUCT_BUNDLE_IDENTIFIER = "com.instagram.IGListKitTests-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; DD3152AA1DE227FA00AC9D2C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_SHADOW = YES; GCC_WARN_UNUSED_PARAMETER = NO; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_CFLAGS = ( "-Wambiguous-macro", "-Wbool-conversion", "-Wcomment", "-Wconditional-uninitialized", "-Wconstant-logical-operand", "-Wcovered-switch-default", "-Wdangling-else", "-Wdeprecated-increment-bool", "-Wdivision-by-zero", "-Wduplicate-enum", "-Wempty-body", "-Wenum-compare", "-Wenum-conversion", "-Wformat", "-Wformat-extra-args", "-Wformat-security", "-Wformat-zero-length", "-Wignored-attributes", "-Wignored-attributes", "-Wimplicit-atomic-properties", "-Wimplicit-fallthrough", "-Wimplicit-retain-self", "-Wincomplete-implementation", "-Wloop-analysis", "-Wmismatched-return-types", "-Wnewline-eof", "-Wnonnull", "-Wnull-character", "-Wobjc-method-access", "-Wobjc-missing-super-calls", "-Wreturn-type", "-Wself-assign", "-Wswitch", "-Wswitch-bool", "-Wswitch-enum", "-Wunicode", "-Wunknown-pragmas", "-Wunused-const-variable", "-Wunused-function", "-Wunused-label", "-Wunused-property-ivar", "-Wunused-result", "-Wunused-value", "-Wnullable-to-nonnull-conversion", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = IGListKit; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSiON = 5.0; }; name = Debug; }; DD3152AB1DE227FA00AC9D2C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_SHADOW = YES; GCC_WARN_UNUSED_PARAMETER = NO; INFOPLIST_FILE = "$(SRCROOT)/Source/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; OTHER_CFLAGS = ( "-Wambiguous-macro", "-Wbool-conversion", "-Wcomment", "-Wconditional-uninitialized", "-Wconstant-logical-operand", "-Wcovered-switch-default", "-Wdangling-else", "-Wdeprecated-increment-bool", "-Wdivision-by-zero", "-Wduplicate-enum", "-Wempty-body", "-Wenum-compare", "-Wenum-conversion", "-Wformat", "-Wformat-extra-args", "-Wformat-security", "-Wformat-zero-length", "-Wignored-attributes", "-Wignored-attributes", "-Wimplicit-atomic-properties", "-Wimplicit-fallthrough", "-Wimplicit-retain-self", "-Wincomplete-implementation", "-Wloop-analysis", "-Wmismatched-return-types", "-Wnewline-eof", "-Wnonnull", "-Wnull-character", "-Wobjc-method-access", "-Wobjc-missing-super-calls", "-Wreturn-type", "-Wself-assign", "-Wswitch", "-Wswitch-bool", "-Wswitch-enum", "-Wunicode", "-Wunknown-pragmas", "-Wunused-const-variable", "-Wunused-function", "-Wunused-label", "-Wunused-property-ivar", "-Wunused-result", "-Wunused-value", "-Wnullable-to-nonnull-conversion", ); PRODUCT_BUNDLE_IDENTIFIER = com.instagram.IGListKit; PRODUCT_NAME = IGListKit; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_TREAT_WARNINGS_AS_ERRORS = YES; SWIFT_VERSiON = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 7A02D0222361520200B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A02D0232361520200B49FAE /* Debug */, 7A02D0242361520200B49FAE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7A02D0302361525800B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit-tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A02D0312361525800B49FAE /* Debug */, 7A02D0322361525800B49FAE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7A02D03D2361526600B49FAE /* Build configuration list for PBXNativeTarget "IGListDiffKit-macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A02D03E2361526600B49FAE /* Debug */, 7A02D03F2361526600B49FAE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7A92EA9C23A8A42000E5A13D /* Build configuration list for PBXNativeTarget "IGListSwiftKit" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A92EA9D23A8A42000E5A13D /* Debug */, 7A92EA9E23A8A42000E5A13D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7A92EAAD23A8A50100E5A13D /* Build configuration list for PBXNativeTarget "IGListSwiftKit-tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 7A92EAAE23A8A50100E5A13D /* Debug */, 7A92EAAF23A8A50100E5A13D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 885FE1E91DC51A0D009CE2B4 /* Build configuration list for PBXNativeTarget "IGListKit-tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( 885FE1E51DC51A0D009CE2B4 /* Debug */, 885FE1E61DC51A0D009CE2B4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 885FE1EA1DC51A0D009CE2B4 /* Build configuration list for PBXNativeTarget "IGListKit-tvOSTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 885FE1E71DC51A0D009CE2B4 /* Debug */, 885FE1E81DC51A0D009CE2B4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 887D0B301D870D7E009E01F7 /* Build configuration list for PBXProject "IGListKit" */ = { isa = XCConfigurationList; buildConfigurations = ( 887D0B481D870D7F009E01F7 /* Debug */, 887D0B491D870D7F009E01F7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 887D0B4A1D870D7F009E01F7 /* Build configuration list for PBXNativeTarget "IGListKit" */ = { isa = XCConfigurationList; buildConfigurations = ( 887D0B4B1D870D7F009E01F7 /* Debug */, 887D0B4C1D870D7F009E01F7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 887D0B4D1D870D7F009E01F7 /* Build configuration list for PBXNativeTarget "IGListKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 887D0B4E1D870D7F009E01F7 /* Debug */, 887D0B4F1D870D7F009E01F7 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 88DF89861E010E6A00B1B9B4 /* Build configuration list for PBXNativeTarget "IGListKit-macOSTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 88DF89841E010E6A00B1B9B4 /* Debug */, 88DF89851E010E6A00B1B9B4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; DD3152A91DE227FA00AC9D2C /* Build configuration list for PBXNativeTarget "IGListKit-macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( DD3152AA1DE227FA00AC9D2C /* Debug */, DD3152AB1DE227FA00AC9D2C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ F12D8EA82E93852600A7DDBE /* XCRemoteSwiftPackageReference "ocmock" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/erikdoe/ocmock.git"; requirement = { branch = master; kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ F12D8EA92E93852600A7DDBE /* OCMock */ = { isa = XCSwiftPackageProductDependency; package = F12D8EA82E93852600A7DDBE /* XCRemoteSwiftPackageReference "ocmock" */; productName = OCMock; }; F12D8EAB2E93853500A7DDBE /* OCMock */ = { isa = XCSwiftPackageProductDependency; package = F12D8EA82E93852600A7DDBE /* XCRemoteSwiftPackageReference "ocmock" */; productName = OCMock; }; F12D8EAE2E93854100A7DDBE /* OCMock */ = { isa = XCSwiftPackageProductDependency; package = F12D8EA82E93852600A7DDBE /* XCRemoteSwiftPackageReference "ocmock" */; productName = OCMock; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 887D0B2D1D870D7E009E01F7 /* Project object */; } ================================================ FILE: IGListKit.xcodeproj/xcshareddata/xcschemes/IGListKit-macOS.xcscheme ================================================ ================================================ FILE: IGListKit.xcodeproj/xcshareddata/xcschemes/IGListKit-tvOS.xcscheme ================================================ ================================================ FILE: IGListKit.xcodeproj/xcshareddata/xcschemes/IGListKit.xcscheme ================================================ ================================================ FILE: IGListSwiftKit.podspec ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. Pod::Spec.new do |s| s.name = 'IGListSwiftKit' s.version = `scripts/version.sh` s.summary = 'A data-driven UICollectionView framework.' s.homepage = 'https://github.com/Instagram/IGListKit' s.documentation_url = 'https://instagram.github.io/IGListKit' s.description = 'A data-driven UICollectionView framework for building fast and flexible lists.' s.license = { :type => 'MIT' } s.authors = 'Instagram' s.social_media_url = 'https://twitter.com/fbOpenSource' s.source = { :git => 'https://github.com/Instagram/IGListKit.git', :tag => s.version.to_s, :branch => 'stable' } s.dependency 'IGListKit', "= #{s.version}" [s.ios, s.tvos].each do |os| os.source_files = [ 'Source/IGListSwiftKit/**/*.{swift}', ] end s.requires_arc = true s.swift_versions = ['4.0', '5.0', '5.1'] s.ios.deployment_target = '11.0' s.tvos.deployment_target = '11.0' s.ios.frameworks = 'UIKit' s.tvos.frameworks = 'UIKit' end ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) Meta Platforms, Inc. and affiliates. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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 THE AUTHORS OR COPYRIGHT HOLDERS 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. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "IGListKit", platforms: [ .iOS(.v11), .tvOS(.v11), .macOS(.v10_13), ], products: [ .library(name: "IGListDiffKit", targets: ["IGListDiffKit"]), .library(name: "IGListKit", targets: ["IGListKit"]), .library(name: "IGListSwiftKit", targets: ["IGListSwiftKit"]), ], targets: [ .target( name: "IGListDiffKit", path: "spm/Sources/IGListDiffKit", publicHeadersPath: "include" ), .target( name: "IGListKit", dependencies: ["IGListDiffKit"], path: "spm/Sources/IGListKit", publicHeadersPath: "include" ), .target( name: "IGListSwiftKit", dependencies: ["IGListKit"], path: "spm/Sources/IGListSwiftKit" ), ], cLanguageStandard: .c11, cxxLanguageStandard: .cxx11 ) ================================================ FILE: README.md ================================================

Build Status Coverage Status Pods Version Platforms Carthage Compatible

---------------- A data-driven `UICollectionView` framework for building fast and flexible lists. | | Main Features | ----------|----------------- 🙅 | Never call `performBatchUpdates(_:, completion:)` or `reloadData()` again 🏠 | Better architecture with reusable cells and components 🔠 | Create collections with multiple data types 🔑 | Decoupled diffing algorithm ✅ | Fully unit tested 🔍 | Customize your diffing behavior for your models 📱 | Simply `UICollectionView` at its core 🚀 | Extendable API 🐦 | Written in Objective-C with full Swift interop support `IGListKit` is built and maintained with ❤️ by [Instagram engineering](https://engineering.instagram.com/). We use the open source version `main` branch in the Instagram app. ## Multilingual translation [Chinese README](README.zh.md) ## Requirements - Swift 5.1+ - iOS 11.0+ - tvOS 11.0+ - macOS 10.13+ *(diffing algorithm components only)* - Interoperability with Swift 3.0+ ## Installation ### CocoaPods The preferred installation method is with [CocoaPods](https://cocoapods.org). Add the following to your `Podfile`: ```ruby pod 'IGListKit', '~> 5.2.0' ``` ### Carthage For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`: ```ogdl github "Instagram/IGListKit" ~> 5.2.0 ``` ### Swift Package Manager For [Swift Package Manager](https://swift.org/package-manager/): ``` To integrate using Xcode: File -> Swift Packages -> Add Package Dependency Enter package URL: https://github.com/Instagram/IGListKit, and select the latest release. ``` > For advanced usage, see our [Installation Guide](https://instagram.github.io/IGListKit/installation.html). ## Getting Started Try out IGListKit by opening any of the sample apps available in the `Examples ` directory. - Our [Getting Started guide](https://instagram.github.io/IGListKit/getting-started.html) - Ray Wenderlich's [IGListKit Tutorial: Better UICollectionViews](https://www.raywenderlich.com/147162/iglistkit-tutorial-better-uicollectionviews) - Our [example projects](https://github.com/Instagram/IGListKit/tree/main/Examples) - Ryan Nystrom's [talk at try! Swift NYC](https://academy.realm.io/posts/tryswift-ryan-nystrom-refactoring-at-scale-lessons-learned-rewriting-instagram-feed/)(Note: this talk was for an earlier version. Some APIs have changed.) - [Migrating an UITableView to IGListCollectionView](https://medium.com/cocoaacademymag/iglistkit-migrating-an-uitableview-to-iglistkitcollectionview-65a30cf9bac9), by Rodrigo Cavalcante - [Keeping data fresh in Buffer for iOS with AsyncDisplayKit, IGListKit & Pusher](https://overflow.buffer.com/2017/04/10/keeping-data-fresh-buffer-ios-asyncdisplaykit-iglistkit-pusher/), Andy Yates, Buffer ## Documentation You can find [the docs here](https://instagram.github.io/IGListKit). Documentation is generated with [jazzy](https://github.com/realm/jazzy) and hosted on [GitHub-Pages](https://pages.github.com). To regenerate docs, run `./scripts/build_docs.sh` from the root directory in the repo. ## Vision For the long-term goals and "vision" of `IGListKit`, please read our [Vision](https://github.com/Instagram/IGListKit/blob/main/Guides/VISION.md) doc. ## Contributing Please see the [CONTRIBUTING](https://github.com/Instagram/IGListKit/blob/main/.github/CONTRIBUTING.md) file for how to help. At Instagram, we sync the open source version of `IGListKit` daily, so we're always testing the latest changes. But that requires all changes be thoroughly tested and follow our style guide. We have a set of [starter tasks](https://github.com/Instagram/IGListKit/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task) that are great for beginners to jump in on and start contributing. ## License `IGListKit` is [MIT-licensed](./LICENSE.md). The files in the `/Examples/` directory are licensed under a separate license as specified in each file. Documentation is licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/). ## Legal Copyright © Meta Platforms, Inc • Terms of UsePrivacy Policy ================================================ FILE: README.zh.md ================================================

Build Status Coverage Status Pods Version Platforms Carthage Compatible

---------------- 一个数据驱动的`UICollectionView`框架,用于构建快速灵活的列表。 | | 主要特性 | ----------|----------------- 🙅 | 无须直接调用 `performBatchUpdates(_:, completion:)` 或 `reloadData()` 🏠 | 更好的可复用 cell 和组件体系结构 🔠 | 创建具有多个数据类型的列表 🔑 | 解耦的 Diff 算法 ✅ | 完全的单元测试 🔍 | 可自定义数据模型的 Diff 行为 📱 | 简化并维持`UICollectionView`的核心特性 🚀 | 可扩展的 API 设计 🐦 | Objective-C 编写,同时完全支持 Swift `IGListKit`由 [Instagram 工程师](https://engineering.instagram.com/) 创建 并且❤️ 维护。 我们在 Instagram 中使用开源的`main`主版本。 ## 多语言翻译 [英文README](README.md) ## 要求 - Swift 5.1+ - iOS 11.0+ - tvOS 11.0+ - macOS 10.13+ *(diffing algorithm components only)* - Interoperability with Swift 3.0+ ## 安装 ### CocoaPods 推荐使用[CocoaPods](https://cocoapods.org)来进行安装,只需添加如下语句到你的`Podfile`文件中: ```ruby pod 'IGListKit', '~> 5.2.0' ``` ### Carthage 对于[Carthage](https://github.com/Carthage/Carthage), 添加如下到`Cartfile`文件中: ```ogdl github "Instagram/IGListKit" ~> 5.2.0 ``` > 对于高级用法, 查阅 [安装指南](https://instagram.github.io/IGListKit/installation.html)。 ## 入门指南 ```bash $ git clone https://github.com/Instagram/IGListKit.git $ cd IGListKit/ $ ./scripts/setup.sh ``` - [入门指南](https://instagram.github.io/IGListKit/getting-started.html) - Ray Wenderlich's [IGListKit Tutorial: Better UICollectionViews](https://www.raywenderlich.com/147162/iglistkit-tutorial-better-uicollectionviews) - [样例项目](https://github.com/Instagram/IGListKit/tree/main/Examples) - Ryan Nystrom's [talk at try! Swift NYC](https://realm.io/news/tryswift-ryan-nystrom-refactoring-at-scale-lessons-learned-rewriting-instagram-feed/) (Note: this talk was for an earlier version. Some APIs have changed.) - [Migrating an UITableView to IGListCollectionView](https://medium.com/cocoaacademymag/iglistkit-migrating-an-uitableview-to-iglistkitcollectionview-65a30cf9bac9), by Rodrigo Cavalcante - [Keeping data fresh in Buffer for iOS with AsyncDisplayKit, IGListKit & Pusher](https://overflow.buffer.com/2017/04/10/keeping-data-fresh-buffer-ios-asyncdisplaykit-iglistkit-pusher/), Andy Yates, Buffer ## 文档 [这里可以查阅文档](https://instagram.github.io/IGListKit)。文档由[jazzy](https://github.com/realm/jazzy)生成,托管在 [GitHub-Pages](https://pages.github.com)。 运行位于仓库根目录下的`./scripts/build_docs.sh`脚本来生成文档。 ## 远景 想要了解`IGListKit`的长期目标和愿景,请阅读[Vision](https://github.com/Instagram/IGListKit/blob/main/Guides/VISION.md)。 ## 贡献 请查看[CONTRIBUTING](https://github.com/Instagram/IGListKit/blob/main/.github/CONTRIBUTING.md)来了解如何参与贡献。在 Instagram,我们每日都会同步开源版本的`IGListKit`,因此我们总是在测试最新的改动。但是这也需要所有的改动都需要经历完全的测试,并且遵守我们的开发风格。 我们有一系列[新人任务](https://github.com/Instagram/IGListKit/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task),来帮助新人学习如何参入其中。 ## License `IGListKit` 遵循[MIT-licensed](./LICENSE)。 `/Examples/`目录下的文件,在文档里指明了它们各自所遵循的协议。文档遵循[CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/)。 ================================================ FILE: Source/IGListDiffKit/IGListAssert.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #ifndef IGAssert #define IGAssert( condition, ... ) NSCAssert( (condition) , ##__VA_ARGS__) #endif // IGAssert #ifndef IGWarn #define IGWarn( condition, ... ) NSCAssert( (condition) , ##__VA_ARGS__) #endif // IGWarn #ifndef IGWarnAssert #define IGWarnAssert( ... ) IGAssert( (NO) , ##__VA_ARGS__) #endif // IGWarnAssert #ifndef IGFailAssert #define IGFailAssert( ... ) IGAssert( (NO) , ##__VA_ARGS__) #endif // IGFailAssert #ifndef IGFailure #define IGFailure( ... ) IGAssert( (NO) , ##__VA_ARGS__) #endif // IGFailure #ifndef IGParameterAssert #define IGParameterAssert( condition ) IGAssert( (condition) , @"Invalid parameter not satisfying: %@", @#condition) #endif // IGParameterAssert #ifndef IGAssertMainThread #define IGAssertMainThread() IGAssert( ([NSThread isMainThread] == YES), @"Must be on the main thread") #endif // IGAssertMainThread #ifndef IG_ASSERTIONS_ENABLED #if !defined(NS_BLOCK_ASSERTIONS) #define IG_ASSERTIONS_ENABLED 1 #else #define IG_ASSERTIONS_ENABLED 0 #endif #endif ================================================ FILE: Source/IGListDiffKit/IGListBatchUpdateData.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #import "IGListMoveIndex.h" #import "IGListMoveIndexPath.h" #else #import #import #import #endif NS_ASSUME_NONNULL_BEGIN /** An instance of `IGListBatchUpdateData` takes section indexes and item index paths and performs cleanup on init in order to perform a crash-free update via `-[UICollectionView performBatchUpdates:completion:]`. */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_NAME(ListBatchUpdateData) @interface IGListBatchUpdateData : NSObject /** Section insert indexes. */ @property (nonatomic, strong, readonly) NSIndexSet *insertSections; /** Section delete indexes. */ @property (nonatomic, strong, readonly) NSIndexSet *deleteSections; /** Section moves. */ @property (nonatomic, strong, readonly) NSSet *moveSections; /** Item insert index paths. */ @property (nonatomic, strong, readonly) NSArray *insertIndexPaths; /** Item delete index paths. */ @property (nonatomic, strong, readonly) NSArray *deleteIndexPaths; /** Item update index paths. */ @property (nonatomic, strong, readonly) NSArray *updateIndexPaths; /** Item moves. */ @property (nonatomic, strong, readonly) NSArray *moveIndexPaths; /** Creates a new batch update object with section and item operations. @param insertSections Section indexes to insert. @param deleteSections Section indexes to delete. @param moveSections Section moves. @param insertIndexPaths Item index paths to insert. @param deleteIndexPaths Item index paths to delete. @param updateIndexPaths Item index paths to update. @param moveIndexPaths Item index paths to move. @return A new batch update object. */ - (instancetype)initWithInsertSections:(NSIndexSet *)insertSections deleteSections:(NSIndexSet *)deleteSections moveSections:(NSSet *)moveSections insertIndexPaths:(NSArray *)insertIndexPaths deleteIndexPaths:(NSArray *)deleteIndexPaths updateIndexPaths:(NSArray *)updateIndexPaths moveIndexPaths:(NSArray *)moveIndexPaths NS_DESIGNATED_INITIALIZER; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListBatchUpdateData.mm ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBatchUpdateData.h" #import #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListCompatibility.h" // Plucks the given move from available moves and turns it into a delete + insert static void convertMoveToDeleteAndInsert(NSMutableSet *moves, IGListMoveIndex *move, NSMutableIndexSet *deletes, NSMutableIndexSet *inserts) { [moves removeObject:move]; // add a delete and insert respecting the move's from and to sections // delete + insert will result in reloading the entire section [deletes addIndex:move.from]; [inserts addIndex:move.to]; } @implementation IGListBatchUpdateData // Converts all section moves that have index path operations into a section delete + insert. + (void)_cleanIndexPathsWithMap:(const std::unordered_map &)map moves:(NSMutableSet *)moves indexPaths:(NSMutableArray *)indexPaths deletes:(NSMutableIndexSet *)deletes inserts:(NSMutableIndexSet *)inserts { if (indexPaths.count == 0) { return; } for (NSInteger i = indexPaths.count - 1; i >= 0; i--) { NSIndexPath *path = indexPaths[i]; const auto it = map.find(path.section); if (it != map.end() && it->second != nil) { [indexPaths removeObjectAtIndex:i]; convertMoveToDeleteAndInsert(moves, it->second, deletes, inserts); } } } /** Converts all section moves that are also reloaded, or have index path inserts, deletes, or reloads into a section delete + insert in order to avoid UICollectionView heap corruptions, exceptions, and animation/snapshot bugs. */ - (instancetype)initWithInsertSections:(nonnull NSIndexSet *)insertSections deleteSections:(nonnull NSIndexSet *)deleteSections moveSections:(nonnull NSSet *)moveSections insertIndexPaths:(nonnull NSArray *)insertIndexPaths deleteIndexPaths:(nonnull NSArray *)deleteIndexPaths updateIndexPaths:(nonnull NSArray *)updateIndexPaths moveIndexPaths:(nonnull NSArray *)moveIndexPaths { IGParameterAssert(insertSections != nil); IGParameterAssert(deleteSections != nil); IGParameterAssert(moveSections != nil); IGParameterAssert(insertIndexPaths != nil); IGParameterAssert(deleteIndexPaths != nil); IGParameterAssert(updateIndexPaths != nil); IGParameterAssert(moveIndexPaths != nil); if (self = [super init]) { NSMutableSet *mMoveSections = [moveSections mutableCopy]; NSMutableIndexSet *mDeleteSections = [deleteSections mutableCopy]; NSMutableIndexSet *mInsertSections = [insertSections mutableCopy]; NSMutableSet *mMoveIndexPaths = [moveIndexPaths mutableCopy]; // these collections should NEVER be mutated during cleanup passes, otherwise sections that have multiple item // changes (e.g. a moved section that has a delete + reload on different index paths w/in the section) will only // convert one of the item changes into a section delete+insert. this will fail hard and be VERY difficult to // debug const NSInteger moveCount = [moveSections count]; std::unordered_map fromMap(MAX(moveCount, 1)); std::unordered_map toMap(MAX(moveCount, 1)); for (IGListMoveIndex *move in moveSections) { const NSInteger from = move.from; const NSInteger to = move.to; // if the move is already deleted or inserted, discard it because count-changing operations must match // with data source changes if ([deleteSections containsIndex:from] || [insertSections containsIndex:to]) { [mMoveSections removeObject:move]; } else { fromMap[from] = move; toMap[to] = move; } } NSMutableArray *mDeleteIndexPaths; NSMutableArray *mInsertIndexPaths; // Avoid a flaky UICollectionView bug when deleting from the same index path twice // exposes a possible data source inconsistency issue NSMutableDictionary *const deleteCounts = [NSMutableDictionary new]; // If we need to remove a duplicate delete, we also need to remove an insert to balance the count. // Lets build the delete counts for each index, which we can use to skip corresponding inserts. for (NSIndexPath *deleteIndexPath in deleteIndexPaths) { const NSInteger deleteCount = deleteCounts[deleteIndexPath].integerValue; deleteCounts[deleteIndexPath] = @(deleteCount + 1); } // Skip inserts that have an associated skipped delete NSMutableArray *const trimmedInsertIndexPath = [NSMutableArray new]; for (NSIndexPath *insertIndexPath in insertIndexPaths) { const NSInteger deleteCount = deleteCounts[insertIndexPath].integerValue; if (deleteCount > 1) { // Skip! deleteCounts[insertIndexPath] = @(deleteCount - 1); } else { [trimmedInsertIndexPath addObject:insertIndexPath]; } } mDeleteIndexPaths = [[deleteCounts allKeys] mutableCopy]; mInsertIndexPaths = trimmedInsertIndexPath; // avoids a bug where a cell is animated twice and one of the snapshot cells is never removed from the hierarchy [IGListBatchUpdateData _cleanIndexPathsWithMap:fromMap moves:mMoveSections indexPaths:mDeleteIndexPaths deletes:mDeleteSections inserts:mInsertSections]; // prevents a bug where UICollectionView corrupts the heap memory when inserting into a section that is moved [IGListBatchUpdateData _cleanIndexPathsWithMap:toMap moves:mMoveSections indexPaths:mInsertIndexPaths deletes:mDeleteSections inserts:mInsertSections]; for (IGListMoveIndexPath *move in moveIndexPaths) { // if the section w/ an index path move is deleted, just drop the move if ([deleteSections containsIndex:move.from.section]) { [mMoveIndexPaths removeObject:move]; } // if a move is inside a section that is moved, convert the section move to a delete+insert const auto it = fromMap.find(move.from.section); if (it != fromMap.end() && it->second != nil) { IGListMoveIndex *sectionMove = it->second; [mMoveIndexPaths removeObject:move]; [mMoveSections removeObject:sectionMove]; [mDeleteSections addIndex:sectionMove.from]; [mInsertSections addIndex:sectionMove.to]; } } _deleteSections = [mDeleteSections copy]; _insertSections = [mInsertSections copy]; _moveSections = [mMoveSections copy]; _deleteIndexPaths = [mDeleteIndexPaths copy]; _insertIndexPaths = [mInsertIndexPaths copy]; _updateIndexPaths = [updateIndexPaths copy]; _moveIndexPaths = [mMoveIndexPaths copy]; } return self; } - (BOOL)isEqual:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGListBatchUpdateData class]]) { return ([self.insertSections isEqual:[object insertSections]] && [self.deleteSections isEqual:[object deleteSections]] && [self.moveSections isEqual:[object moveSections]] && [self.insertIndexPaths isEqual:[object insertIndexPaths]] && [self.deleteIndexPaths isEqual:[object deleteIndexPaths]] && [self.updateIndexPaths isEqual:[object updateIndexPaths]] && [self.moveIndexPaths isEqual:[object moveIndexPaths]]); } return NO; } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p; deleteSections: %lu; insertSections: %lu; moveSections: %lu; deleteIndexPaths: %lu; insertIndexPaths: %lu; updateIndexPaths: %lu>", NSStringFromClass(self.class), self, (unsigned long)self.deleteSections.count, (unsigned long)self.insertSections.count, (unsigned long)self.moveSections.count, (unsigned long)self.deleteIndexPaths.count, (unsigned long)self.insertIndexPaths.count, (unsigned long)self.updateIndexPaths.count]; } @end ================================================ FILE: Source/IGListDiffKit/IGListCompatibility.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if TARGET_OS_EMBEDDED || TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST #import #elif TARGET_OS_OSX #import #else #import #endif ================================================ FILE: Source/IGListDiffKit/IGListDiff.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListDiffable.h" #import "IGListIndexPathResult.h" #import "IGListIndexSetResult.h" #else #import #import #import #endif NS_ASSUME_NONNULL_BEGIN /** An option for how to do comparisons between similar objects. */ NS_SWIFT_NAME(ListDiffOption) typedef NS_ENUM(NSInteger, IGListDiffOption) { /** Compare objects using pointer personality. */ IGListDiffPointerPersonality, /** Compare objects using `-[IGListDiffable isEqualToDiffableObject:]`. */ IGListDiffEquality }; /** Creates a diff using indexes between two collections. @param oldArray The old objects to diff against. @param newArray The new objects. @param option An option on how to compare objects. @return A result object containing affected indexes. */ NS_SWIFT_NAME(ListDiff(oldArray:newArray:option:)) FOUNDATION_EXTERN IGListIndexSetResult *IGListDiff(NSArray> *_Nullable oldArray, NSArray> *_Nullable newArray, IGListDiffOption option); /** Creates a diff using index paths between two collections. @param fromSection The old section. @param toSection The new section. @param oldArray The old objects to diff against. @param newArray The new objects. @param option An option on how to compare objects. @return A result object containing affected indexes. */ NS_SWIFT_NAME(ListDiffPaths(fromSection:toSection:oldArray:newArray:option:)) FOUNDATION_EXTERN IGListIndexPathResult *IGListDiffPaths(NSInteger fromSection, NSInteger toSection, NSArray> *_Nullable oldArray, NSArray> *_Nullable newArray, IGListDiffOption option); NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListDiff.mm ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDiff.h" #import #import #import #import "IGListCompatibility.h" #import "IGListMacros.h" #import "IGListIndexPathResultInternal.h" #import "IGListIndexSetResultInternal.h" #import "IGListMoveIndexInternal.h" #import "IGListMoveIndexPathInternal.h" using namespace std; /// Used to track data stats while diffing. struct IGListEntry { /// The number of times the data occurs in the old array NSInteger oldCounter = 0; /// The number of times the data occurs in the new array NSInteger newCounter = 0; /// The indexes of the data in the old array stack oldIndexes; /// Flag marking if the data has been updated between arrays by checking the isEqual: method BOOL updated = NO; }; /// Track both the entry and algorithm index. Default the index to NSNotFound struct IGListRecord { IGListEntry *entry; mutable NSInteger index; IGListRecord() { entry = NULL; index = NSNotFound; } }; static id IGListTableKey(__unsafe_unretained id object) { id key = [object diffIdentifier]; NSCAssert(key != nil, @"Cannot use a nil key for the diffIdentifier of object %@", object); return key; } struct IGListEqualID { bool operator()(const id a, const id b) const { return (a == b) || [a isEqual: b]; } }; struct IGListHashID { size_t operator()(const id o) const { return (size_t)[o hash]; } }; static void addIndexToMap(BOOL useIndexPaths, NSInteger section, NSInteger index, __unsafe_unretained id object, __unsafe_unretained NSMapTable *map) { id value; if (useIndexPaths) { value = [NSIndexPath indexPathForItem:index inSection:section]; } else { value = @(index); } [map setObject:value forKey:[object diffIdentifier]]; } static void addIndexToCollection(BOOL useIndexPaths, __unsafe_unretained id collection, NSInteger section, NSInteger index) { if (useIndexPaths) { NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:section]; [collection addObject:path]; } else { [collection addIndex:index]; } }; static NSArray *indexPathsAndPopulateMap(__unsafe_unretained NSArray> *array, NSInteger section, __unsafe_unretained NSMapTable *map) { NSMutableArray *paths = [NSMutableArray new]; [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:section]; [paths addObject:path]; [map setObject:path forKey:[obj diffIdentifier]]; }]; return paths; } static id IGListDiffing(BOOL returnIndexPaths, NSInteger fromSection, NSInteger toSection, NSArray> *oldArray, NSArray> *newArray, IGListDiffOption option) { const NSInteger newCount = newArray.count; const NSInteger oldCount = oldArray.count; NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable]; NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable]; // if no new objects, everything from the oldArray is deleted // take a shortcut and just build a delete-everything result if (newCount == 0) { if (returnIndexPaths) { return [[IGListIndexPathResult alloc] initWithInserts:[NSArray new] deletes:indexPathsAndPopulateMap(oldArray, fromSection, oldMap) updates:[NSArray new] moves:[NSArray new] oldIndexPathMap:oldMap newIndexPathMap:newMap]; } else { [oldArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { addIndexToMap(returnIndexPaths, fromSection, idx, obj, oldMap); }]; return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet new] deletes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)] updates:[NSIndexSet new] moves:[NSArray new] oldIndexMap:oldMap newIndexMap:newMap]; } } // if no old objects, everything from the newArray is inserted // take a shortcut and just build an insert-everything result if (oldCount == 0) { if (returnIndexPaths) { return [[IGListIndexPathResult alloc] initWithInserts:indexPathsAndPopulateMap(newArray, toSection, newMap) deletes:[NSArray new] updates:[NSArray new] moves:[NSArray new] oldIndexPathMap:oldMap newIndexPathMap:newMap]; } else { [newArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { addIndexToMap(returnIndexPaths, toSection, idx, obj, newMap); }]; return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newCount)] deletes:[NSIndexSet new] updates:[NSIndexSet new] moves:[NSArray new] oldIndexMap:oldMap newIndexMap:newMap]; } } // symbol table uses the old/new array diffIdentifier as the key and IGListEntry as the value // using id as the key provided by https://lists.gnu.org/archive/html/discuss-gnustep/2011-07/msg00019.html unordered_map, IGListEntry, IGListHashID, IGListEqualID> table; // pass 1 // create an entry for every item in the new array // increment its new count for each occurence vector newResultsArray(newCount); for (NSInteger i = 0; i < newCount; i++) { id key = IGListTableKey(newArray[i]); IGListEntry &entry = table[key]; entry.newCounter++; // add NSNotFound for each occurence of the item in the new array entry.oldIndexes.push(NSNotFound); // note: the entry is just a pointer to the entry which is stack-allocated in the table newResultsArray[i].entry = &entry; } // pass 2 // update or create an entry for every item in the old array // increment its old count for each occurence // record the original index of the item in the old array // MUST be done in descending order to respect the oldIndexes stack construction vector oldResultsArray(oldCount); for (NSInteger i = oldCount - 1; i >= 0; i--) { id key = IGListTableKey(oldArray[i]); IGListEntry &entry = table[key]; entry.oldCounter++; // push the original indices where the item occurred onto the index stack entry.oldIndexes.push(i); // note: the entry is just a pointer to the entry which is stack-allocated in the table oldResultsArray[i].entry = &entry; } // pass 3 // handle data that occurs in both arrays for (NSInteger i = 0; i < newCount; i++) { IGListEntry *entry = newResultsArray[i].entry; // grab and pop the top original index. if the item was inserted this will be NSNotFound NSCAssert(!entry->oldIndexes.empty(), @"Old indexes is empty while iterating new item %li. Should have NSNotFound", (long)i); const NSInteger originalIndex = entry->oldIndexes.top(); entry->oldIndexes.pop(); if (originalIndex < oldCount) { const id n = newArray[i]; const id o = oldArray[originalIndex]; switch (option) { case IGListDiffPointerPersonality: // flag the entry as updated if the pointers are not the same if (n != o) { entry->updated = YES; } break; case IGListDiffEquality: // use -[IGListDiffable isEqualToDiffableObject:] between both version of data to see if anything has changed // skip the equality check if both indexes point to the same object if (n != o && ![n isEqualToDiffableObject:o]) { entry->updated = YES; } break; default /* unexpected */: IGLK_UNEXPECTED_SWITCH_CASE_ABORT(IGListDiffOption, option); } } if (originalIndex != NSNotFound && entry->newCounter > 0 && entry->oldCounter > 0) { // if an item occurs in the new and old array, it is unique // assign the index of new and old records to the opposite index (reverse lookup) newResultsArray[i].index = originalIndex; oldResultsArray[originalIndex].index = i; } } // storage for final NSIndexPaths or indexes id mInserts, mMoves, mUpdates, mDeletes; if (returnIndexPaths) { mInserts = [NSMutableArray new]; mMoves = [NSMutableArray new]; mUpdates = [NSMutableArray new]; mDeletes = [NSMutableArray new]; } else { mInserts = [NSMutableIndexSet new]; mMoves = [NSMutableArray new]; mUpdates = [NSMutableIndexSet new]; mDeletes = [NSMutableIndexSet new]; } // track offsets from deleted items to calculate where items have moved vector deleteOffsets(oldCount), insertOffsets(newCount); NSInteger runningOffset = 0; // iterate old array records checking for deletes // incremement offset for each delete for (NSInteger i = 0; i < oldCount; i++) { deleteOffsets[i] = runningOffset; const IGListRecord record = oldResultsArray[i]; // if the record index in the new array doesn't exist, its a delete if (record.index == NSNotFound) { addIndexToCollection(returnIndexPaths, mDeletes, fromSection, i); runningOffset++; } addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap); } // reset and track offsets from inserted items to calculate where items have moved runningOffset = 0; for (NSInteger i = 0; i < newCount; i++) { insertOffsets[i] = runningOffset; const IGListRecord record = newResultsArray[i]; const NSInteger oldIndex = record.index; // add to inserts if the opposing index is NSNotFound if (record.index == NSNotFound) { addIndexToCollection(returnIndexPaths, mInserts, toSection, i); runningOffset++; } else { // note that an entry can be updated /and/ moved if (record.entry->updated) { addIndexToCollection(returnIndexPaths, mUpdates, fromSection, oldIndex); } // calculate the offset and determine if there was a move // if the indexes match, ignore the index const NSInteger insertOffset = insertOffsets[i]; const NSInteger deleteOffset = deleteOffsets[oldIndex]; if ((oldIndex - deleteOffset + insertOffset) != i) { id move; if (returnIndexPaths) { NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection]; NSIndexPath *to = [NSIndexPath indexPathForItem:i inSection:toSection]; move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to]; } else { move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i]; } [mMoves addObject:move]; } } addIndexToMap(returnIndexPaths, toSection, i, newArray[i], newMap); } NSCAssert((oldCount + (NSInteger)[mInserts count] - (NSInteger)[mDeletes count]) == newCount, @"Sanity check failed applying %lu inserts and %lu deletes to old count %li equaling new count %li", (unsigned long)[mInserts count], (unsigned long)[mDeletes count], (long)oldCount, (long)newCount); if (returnIndexPaths) { return [[IGListIndexPathResult alloc] initWithInserts:mInserts deletes:mDeletes updates:mUpdates moves:mMoves oldIndexPathMap:oldMap newIndexPathMap:newMap]; } else { return [[IGListIndexSetResult alloc] initWithInserts:mInserts deletes:mDeletes updates:mUpdates moves:mMoves oldIndexMap:oldMap newIndexMap:newMap]; } } IGListIndexSetResult *IGListDiff(NSArray > *oldArray, NSArray> *newArray, IGListDiffOption option) { return IGListDiffing(NO, 0, 0, oldArray, newArray, option); } IGListIndexPathResult *IGListDiffPaths(NSInteger fromSection, NSInteger toSection, NSArray> *oldArray, NSArray> *newArray, IGListDiffOption option) { return IGListDiffing(YES, fromSection, toSection, oldArray, newArray, option); } ================================================ FILE: Source/IGListDiffKit/IGListDiffKit.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #if !__has_include() #import "IGListCompatibility.h" #else #import #endif /** * Project version number for IGListKit. */ FOUNDATION_EXPORT double IGListKitVersionNumber; /** * Project version string for IGListKit. */ FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; #if !__has_include() #import "IGListAssert.h" #import "IGListBatchUpdateData.h" #import "IGListDiff.h" #import "IGListDiffable.h" #import "IGListExperiments.h" #import "IGListIndexPathResult.h" #import "IGListIndexSetResult.h" #import "IGListMoveIndex.h" #import "IGListMoveIndexPath.h" #import "NSNumber+IGListDiffable.h" #import "NSString+IGListDiffable.h" #else #import #import #import #import #import #import #import #import #import #import #import #endif ================================================ FILE: Source/IGListDiffKit/IGListDiffable.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import /** The `IGListDiffable` protocol provides methods needed to compare the identity and equality of two objects. */ NS_SWIFT_NAME(ListDiffable) @protocol IGListDiffable /** Returns a key that uniquely identifies the object. @return A key that can be used to uniquely identify the object. @note Two objects may share the same identifier, but are not equal. A common pattern is to use the `NSObject` category for automatic conformance. However this means that objects will be identified on their pointer value so finding updates becomes impossible. @warning This value should never be mutated. */ - (nonnull id)diffIdentifier; /** Returns whether the receiver and a given object are equal. @param object The object to be compared to the receiver. @return `YES` if the receiver and object are equal, otherwise `NO`. */ - (BOOL)isEqualToDiffableObject:(nullable id)object; @end ================================================ FILE: Source/IGListDiffKit/IGListExperiments.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListDiff.h" #else #import #endif /** Bitmask-able options used for pre-release feature testing. */ NS_SWIFT_NAME(ListExperiment) typedef NS_OPTIONS (NSInteger, IGListExperiment) { /// Specifies no experiments. IGListExperimentNone = 1 << 1, /// Test invalidating layout when cell reloads/updates in IGListBindingSectionController. IGListExperimentInvalidateLayoutForUpdates = 1 << 2, /// Throw NSInternalInconsistencyException during an update IGListExperimentThrowOnInconsistencyException = 1 << 3, /// Remove the early exit so multiple updates can't happen at once IGListExperimentRemoveDataSourceChangeEarlyExit = 1 << 4, /// Avoids creating off-screen cells IGListExperimentFixPreferredFocusedView = 1 << 5, }; /** Customize how diffing is performed */ NS_SWIFT_NAME(ListAdaptiveDiffingExperimentConfig) typedef struct IGListAdaptiveDiffingExperimentConfig { /// Enabled experimental code path. This needs to be enabled for the other properties to take effect. BOOL enabled; /// Enable higher background thread priority BOOL higherQOSEnabled; /// If both item counts are under this number, we'll run the diffing on the main thread. NSInteger maxItemCountToRunOnMain; /// Lower QOS if view is not visible according to `IGListViewVisibilityTracker` BOOL lowerPriorityWhenViewNotVisible; } IGListAdaptiveDiffingExperimentConfig; /** Customize how coalescing works to speed up some updates */ NS_SWIFT_NAME(ListAdaptiveCoalescingExperimentConfig) typedef struct IGListAdaptiveCoalescingExperimentConfig { /// Enable adaptive coalescing, where we try to mininimize the update delay BOOL enabled; /// Start coalescing if the last update was within this interval NSTimeInterval minInterval; /// If we need to coalesce, increase the interval by this much for next time. NSTimeInterval intervalIncrement; /// This is the maximum coalesce interval, so the slowest and update can wait. NSTimeInterval maxInterval; /// Coalece using `maxInterval` if view is not visible according to `IGListViewVisibilityTracker` BOOL useMaxIntervalWhenViewNotVisible; } IGListAdaptiveCoalescingExperimentConfig; /** Check if an experiment is enabled in a bitmask. @param mask The bitmask of experiments. @param option The option to compare with. @return `YES` if the option is in the bitmask, otherwise `NO`. */ NS_SWIFT_NAME(ListExperimentEnabled(mask:option:)) static inline BOOL IGListExperimentEnabled(IGListExperiment mask, IGListExperiment option) { return (mask & option) != 0; } NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListIndexPathResult.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMoveIndexPath.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** A result object returned when diffing with sections. */ NS_SWIFT_NAME(ListIndexPathResult) @interface IGListIndexPathResult : NSObject /** The index paths inserted into the new collection. */ @property (nonatomic, copy, readonly) NSArray *inserts; /** The index paths deleted from the old collection. */ @property (nonatomic, copy, readonly) NSArray *deletes; /** The index paths in the old collection that need updated. */ @property (nonatomic, copy, readonly) NSArray *updates; /** The moves from an index path in the old collection to an index path in the new collection. */ @property (nonatomic, copy, readonly) NSArray *moves; /** A Read-only boolean that indicates whether the result has any changes or not. `YES` if the result has changes, `NO` otherwise. */ @property (nonatomic, assign, readonly) BOOL hasChanges; /** Returns the index path of the object with the specified identifier *before* the diff. @param identifier The diff identifier of the object. @return The index path of the object before the diff, or `nil`. @see `-[IGListDiffable diffIdentifier]`. */ - (nullable NSIndexPath *)oldIndexPathForIdentifier:(id)identifier; /** Returns the index path of the object with the specified identifier *after* the diff. @param identifier The diff identifier of the object. @return The index path of the object after the diff, or `nil`. @see `-[IGListDiffable diffIdentifier]`. */ - (nullable NSIndexPath *)newIndexPathForIdentifier:(id)identifier; /** Creates a new result object with operations safe for use in `UITableView` and `UICollectionView` batch updates. */ - (IGListIndexPathResult *)resultForBatchUpdates; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListIndexPathResult.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListIndexPathResult.h" #import "IGListIndexPathResultInternal.h" @implementation IGListIndexPathResult { NSMapTable, NSIndexPath *> *_oldIndexPathMap; NSMapTable, NSIndexPath *> *_newIndexPathMap; } - (instancetype)initWithInserts:(NSArray *)inserts deletes:(NSArray *)deletes updates:(NSArray *)updates moves:(NSArray *)moves oldIndexPathMap:(NSMapTable, NSIndexPath *> *)oldIndexPathMap newIndexPathMap:(NSMapTable, NSIndexPath *> *)newIndexPathMap { if (self = [super init]) { _inserts = inserts; _deletes = deletes; _updates = updates; _moves = moves; _oldIndexPathMap = oldIndexPathMap; _newIndexPathMap = newIndexPathMap; } return self; } - (BOOL)hasChanges { return self.changeCount > 0; } - (NSInteger)changeCount { return self.inserts.count + self.deletes.count + self.updates.count + self.moves.count; } - (IGListIndexPathResult *)resultForBatchUpdates { NSMutableSet *deletes = [NSMutableSet setWithArray:self.deletes]; NSMutableSet *inserts = [NSMutableSet setWithArray:self.inserts]; NSMutableSet *filteredUpdates = [NSMutableSet setWithArray:self.updates]; NSArray *moves = self.moves; NSMutableArray *filteredMoves = [moves mutableCopy]; // convert move+update to delete+insert, respecting the from/to of the move const NSInteger moveCount = moves.count; for (NSInteger i = moveCount - 1; i >= 0; i--) { IGListMoveIndexPath *move = moves[i]; if ([filteredUpdates containsObject:move.from]) { [filteredMoves removeObjectAtIndex:i]; [filteredUpdates removeObject:move.from]; [deletes addObject:move.from]; [inserts addObject:move.to]; } } // iterate all new identifiers. if its index is updated, delete from the old index and insert the new index for (id key in [_oldIndexPathMap keyEnumerator]) { NSIndexPath *indexPath = [_oldIndexPathMap objectForKey:key]; if ([filteredUpdates containsObject:indexPath]) { [deletes addObject:indexPath]; [inserts addObject:(id)[_newIndexPathMap objectForKey:key]]; } } return [[IGListIndexPathResult alloc] initWithInserts:[inserts allObjects] deletes:[deletes allObjects] updates:[NSArray new] moves:filteredMoves oldIndexPathMap:_oldIndexPathMap newIndexPathMap:_newIndexPathMap]; } - (NSIndexPath *)oldIndexPathForIdentifier:(id)identifier { return [_oldIndexPathMap objectForKey:identifier]; } - (NSIndexPath *)newIndexPathForIdentifier:(id)identifier { return [_newIndexPathMap objectForKey:identifier]; } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p; %lu inserts; %lu deletes; %lu updates; %lu moves>", NSStringFromClass(self.class), self, (unsigned long)self.inserts.count, (unsigned long)self.deletes.count, (unsigned long)self.updates.count, (unsigned long)self.moves.count]; } @end ================================================ FILE: Source/IGListDiffKit/IGListIndexSetResult.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMoveIndex.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** A result object returned when diffing with indexes. */ NS_SWIFT_NAME(ListIndexSetResult) @interface IGListIndexSetResult : NSObject /** The indexes inserted into the new collection. */ @property (nonatomic, strong, readonly) NSIndexSet *inserts; /** The indexes deleted from the old collection. */ @property (nonatomic, strong, readonly) NSIndexSet *deletes; /** The indexes in the old collection that need updated. */ @property (nonatomic, strong, readonly) NSIndexSet *updates; /** The moves from an index in the old collection to an index in the new collection. */ @property (nonatomic, copy, readonly) NSArray *moves; /** A Read-only boolean that indicates whether the result has any changes or not. `YES` if the result has changes, `NO` otherwise. */ @property (nonatomic, assign, readonly) BOOL hasChanges; /** Returns the index of the object with the specified identifier *before* the diff. @param identifier The diff identifier of the object. @return The index of the object before the diff, or `NSNotFound`. @see `-[IGListDiffable diffIdentifier]`. */ - (NSInteger)oldIndexForIdentifier:(id)identifier; /** Returns the index of the object with the specified identifier *after* the diff. @param identifier The diff identifier of the object. @return The index path of the object after the diff, or `NSNotFound`. @see `-[IGListDiffable diffIdentifier]`. */ - (NSInteger)newIndexForIdentifier:(id)identifier; /** Creates a new result object with operations safe for use in `UITableView` and `UICollectionView` batch updates. */ - (IGListIndexSetResult *)resultForBatchUpdates; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListIndexSetResult.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListIndexSetResult.h" #import "IGListIndexSetResultInternal.h" @implementation IGListIndexSetResult { NSMapTable, NSNumber *> *_oldIndexMap; NSMapTable, NSNumber *> *_newIndexMap; } - (instancetype)initWithInserts:(NSIndexSet *)inserts deletes:(NSIndexSet *)deletes updates:(NSIndexSet *)updates moves:(NSArray *)moves oldIndexMap:(NSMapTable, NSNumber *> *)oldIndexMap newIndexMap:(NSMapTable, NSNumber *> *)newIndexMap { if (self = [super init]) { _inserts = inserts; _deletes = deletes; _updates = updates; _moves = moves; _oldIndexMap = oldIndexMap; _newIndexMap = newIndexMap; } return self; } - (BOOL)hasChanges { return self.changeCount > 0; } - (NSInteger)changeCount { return self.inserts.count + self.deletes.count + self.updates.count + self.moves.count; } - (IGListIndexSetResult *)resultForBatchUpdates { NSMutableIndexSet *deletes = [self.deletes mutableCopy]; NSMutableIndexSet *inserts = [self.inserts mutableCopy]; NSMutableIndexSet *filteredUpdates = [self.updates mutableCopy]; NSArray *moves = self.moves; NSMutableArray *filteredMoves = [moves mutableCopy]; // convert all update+move to delete+insert const NSInteger moveCount = moves.count; for (NSInteger i = moveCount - 1; i >= 0; i--) { IGListMoveIndex *move = moves[i]; if ([filteredUpdates containsIndex:move.from]) { [filteredMoves removeObjectAtIndex:i]; [filteredUpdates removeIndex:move.from]; [deletes addIndex:move.from]; [inserts addIndex:move.to]; } } // iterate all new identifiers. if its index is updated, delete from the old index and insert the new index for (id key in [_oldIndexMap keyEnumerator]) { const NSInteger index = [[_oldIndexMap objectForKey:key] integerValue]; if ([filteredUpdates containsIndex:index]) { [deletes addIndex:index]; [inserts addIndex:[[_newIndexMap objectForKey:key] integerValue]]; } } return [[IGListIndexSetResult alloc] initWithInserts:inserts deletes:deletes updates:[NSIndexSet new] moves:filteredMoves oldIndexMap:_oldIndexMap newIndexMap:_newIndexMap]; } - (NSInteger)oldIndexForIdentifier:(id)identifier { NSNumber *index = [_oldIndexMap objectForKey:identifier]; return index == nil ? NSNotFound : [index integerValue]; } - (NSInteger)newIndexForIdentifier:(id)identifier { NSNumber *index = [_newIndexMap objectForKey:identifier]; return index == nil ? NSNotFound : [index integerValue]; } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p; %lu inserts; %lu deletes; %lu updates; %lu moves>", NSStringFromClass(self.class), self, (unsigned long)self.inserts.count, (unsigned long)self.deletes.count, (unsigned long)self.updates.count, (unsigned long)self.moves.count]; } @end ================================================ FILE: Source/IGListDiffKit/IGListMacros.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #ifndef IGLK_SUBCLASSING_RESTRICTED #if defined(__has_attribute) && __has_attribute(objc_subclassing_restricted) #define IGLK_SUBCLASSING_RESTRICTED __attribute__((objc_subclassing_restricted)) #else #define IGLK_SUBCLASSING_RESTRICTED #endif // #if defined(__has_attribute) && __has_attribute(objc_subclassing_restricted) #endif // #ifndef IGLK_SUBCLASSING_RESTRICTED #ifndef IGLK_UNAVAILABLE #define IGLK_UNAVAILABLE(message) __attribute__((unavailable(message))) #endif // #ifndef IGLK_UNAVAILABLE #if defined(IGLK_LOGGING_ENABLED) && IGLK_LOGGING_ENABLED #define IGLKLog( s, ... ) do { NSLog( @"IGListKit: %@", [NSString stringWithFormat: (s), ##__VA_ARGS__] ); } while(0) #else #define IGLKLog( s, ... ) #endif #ifndef IGLK_DEBUG_DESCRIPTION_ENABLED #ifdef DEBUG #define IGLK_DEBUG_DESCRIPTION_ENABLED DEBUG #else #define IGLK_DEBUG_DESCRIPTION_ENABLED 0 #endif // #ifdef DEBUG #endif // #ifndef IGLK_DEBUG_DESCRIPTION_ENABLED #define IGLK_BLOCK_CALL_SAFE(BLOCK, ...) \ do { \ __typeof(BLOCK) ig_safe_block = (BLOCK); \ if (ig_safe_block) { \ ig_safe_block(__VA_ARGS__); \ } \ } while (NO) /* E.g. switch (direction) { case UICollectionViewScrollDirectionHorizontal: ... case UICollectionViewScrollDirectionVertical: ... default: IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } */ #define IGLK_UNEXPECTED_SWITCH_CASE_ABORT(type, value) ({ \ type value__##__LINE__ = (value); \ fprintf(stderr, "Unexpected " #type " : %ld\n", (long)(value__##__LINE__)); \ abort(); \ }) ================================================ FILE: Source/IGListDiffKit/IGListMoveIndex.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** An object representing a move between indexes. */ NS_SWIFT_NAME(ListMoveIndex) @interface IGListMoveIndex : NSObject /** An index in the old collection. */ @property (nonatomic, assign, readonly) NSInteger from; /** An index in the new collection. */ @property (nonatomic, assign, readonly) NSInteger to; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListMoveIndex.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListMoveIndex.h" @implementation IGListMoveIndex - (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to { if (self = [super init]) { _from = from; _to = to; } return self; } - (NSUInteger)hash { return _from ^ _to; } - (BOOL)isEqual:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGListMoveIndex class]]) { const NSInteger f1 = self.from, f2 = [object from]; const NSInteger t1 = self.to, t2 = [object to]; return f1 == f2 && t1 == t2; } return NO; } - (NSComparisonResult)compare:(id)object { const NSInteger right = [object from]; const NSInteger left = [self from]; if (left == right) { return NSOrderedSame; } else if (left < right) { return NSOrderedAscending; } else { return NSOrderedDescending; } } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p; from: %li; to: %li;>", NSStringFromClass(self.class), self, (long)self.from, (long)self.to]; } @end ================================================ FILE: Source/IGListDiffKit/IGListMoveIndexPath.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** An object representing a move between indexes. */ NS_SWIFT_NAME(ListMoveIndexPath) @interface IGListMoveIndexPath : NSObject /** An index path in the old collection. */ @property (nonatomic, strong, readonly) NSIndexPath *from; /** An index path in the new collection. */ @property (nonatomic, strong, readonly) NSIndexPath *to; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/IGListMoveIndexPath.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListMoveIndexPath.h" @implementation IGListMoveIndexPath - (instancetype)initWithFrom:(NSIndexPath *)from to:(NSIndexPath *)to { NSParameterAssert(from != nil); NSParameterAssert(to != nil); if (self = [super init]) { _from = from; _to = to; } return self; } - (NSUInteger)hash { return [_from hash] ^ [_to hash]; } - (BOOL)isEqual:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGListMoveIndexPath class]]) { NSIndexPath *f1 = self.from, *f2 = [object from]; NSIndexPath *t1 = self.to, *t2 = [object to]; return [f1 isEqual:f2] && [t1 isEqual:t2]; } return NO; } - (NSComparisonResult)compare:(id)object { return [[self from] compare:[object from]]; } - (NSString *)description { return [NSString stringWithFormat:@"<%@ %p; from: %@; to: %@;>", NSStringFromClass(self.class), self, self.from, self.to]; } @end ================================================ FILE: Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListIndexPathResult.h" NS_ASSUME_NONNULL_BEGIN @interface IGListIndexPathResult() - (instancetype)initWithInserts:(NSArray *)inserts deletes:(NSArray *)deletes updates:(NSArray *)updates moves:(NSArray *)moves oldIndexPathMap:(NSMapTable, NSIndexPath *> *)oldIndexPathMap newIndexPathMap:(NSMapTable, NSIndexPath *> *)newIndexPathMap; @property (nonatomic, assign, readonly) NSInteger changeCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListIndexSetResult.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN @interface IGListIndexSetResult() - (instancetype)initWithInserts:(NSIndexSet *)inserts deletes:(NSIndexSet *)deletes updates:(NSIndexSet *)updates moves:(NSArray *)moves oldIndexMap:(NSMapTable, NSNumber *> *)oldIndexMap newIndexMap:(NSMapTable, NSNumber *> *)newIndexMap; @property (nonatomic, assign, readonly) NSInteger changeCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListMoveIndex.h" NS_ASSUME_NONNULL_BEGIN @interface IGListMoveIndex () - (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN @interface IGListMoveIndexPath () - (instancetype)initWithFrom:(NSIndexPath *)from to:(NSIndexPath *)to NS_DESIGNATED_INITIALIZER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListDiffKit/NSNumber+IGListDiffable.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListDiffable.h" #else #import #endif /** This category provides default `IGListDiffable` conformance for `NSNumber`. */ @interface NSNumber (IGListDiffable) @end ================================================ FILE: Source/IGListDiffKit/NSNumber+IGListDiffable.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "NSNumber+IGListDiffable.h" @implementation NSNumber (IGListDiffable) - (id)diffIdentifier { return self; } - (BOOL)isEqualToDiffableObject:(id)object { return [self isEqual:object]; } @end ================================================ FILE: Source/IGListDiffKit/NSString+IGListDiffable.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListDiffable.h" #else #import #endif /** This category provides default `IGListDiffable` conformance for `NSString`. */ @interface NSString (IGListDiffable) @end ================================================ FILE: Source/IGListDiffKit/NSString+IGListDiffable.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "NSString+IGListDiffable.h" @implementation NSString (IGListDiffable) - (id)diffIdentifier { return self; } - (BOOL)isEqualToDiffableObject:(id)object { return [self isEqual:object]; } @end ================================================ FILE: Source/IGListKit/IGListAdapter.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListExperiments.h" #import "IGListMacros.h" #else #import #import #endif #if !__has_include() #import "IGListAdapterDataSource.h" #import "IGListAdapterDelegate.h" #import "IGListAdapterMoveDelegate.h" #import "IGListAdapterPerformanceDelegate.h" #import "IGListAdapterUpdateListener.h" #else #import #import #import #import #import #endif @protocol IGListUpdatingDelegate; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** A block to execute when the list updates are completed. @param finished Specifies whether or not the update animations completed successfully. */ NS_SWIFT_NAME(ListUpdaterCompletion) typedef void (^IGListUpdaterCompletion)(BOOL finished); /** `IGListAdapter` objects provide an abstraction for feeds of objects in a `UICollectionView` by breaking each object into individual sections, called "section controllers". These controllers (objects subclassing to `IGListSectionController`) act as a data source and delegate for each section. Feed implementations must act as the data source for an `IGListAdapter` in order to drive the objects and section controllers in a collection view. */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapter) @interface IGListAdapter : NSObject /** The view controller that houses the adapter. */ @property (nonatomic, nullable, weak) UIViewController *viewController; /** The collection view used with the adapter. @note Setting this property will automatically set isPrefetchingEnabled to `NO` for performance reasons. */ @property (nonatomic, nullable, weak) UICollectionView *collectionView; /** The object that acts as the data source for the adapter. */ @property (nonatomic, nullable, weak) id dataSource; /** The object that receives top-level events for section controllers. */ @property (nonatomic, nullable, weak) id delegate; /** The object that receives `UICollectionViewDelegate` events. @note This object *will not* receive `UIScrollViewDelegate` events. Instead use scrollViewDelegate. */ @property (nonatomic, nullable, weak) id collectionViewDelegate; /** The object that receives `UIScrollViewDelegate` events. */ @property (nonatomic, nullable, weak) id scrollViewDelegate; /** The object that receives `IGListAdapterMoveDelegate` events resulting from interactive reordering of sections. @note This works with UICollectionView interactive reordering available on iOS 9.0+ */ @property (nonatomic, nullable, weak) id moveDelegate NS_AVAILABLE_IOS(9_0); /** The object that receives `IGListAdapterPerformanceDelegate` events to measure performance. */ @property (nonatomic, nullable, weak) id performanceDelegate; /** The updater for the adapter. */ @property (nonatomic, strong, readonly) id updater; /** A bitmask of experiments to conduct on the adapter. */ @property (nonatomic, assign) IGListExperiment experiments; /** When true, we automatically deselect cells after they are selected (animated), so each section-controller doesn't have to. Note this doesn't apply to selection via `-selectItemAtIndexPath`, since those do not receive a delegate callback. Default is false. */ @property (nonatomic, assign) BOOL autoDeselectEnabled; /** Initializes a new `IGListAdapter` object. @param updater An object that manages updates to the collection view. @param viewController The view controller that will house the adapter. @param workingRangeSize The number of objects before and after the viewport to consider within the working range. @return A new list adapter object. @note The working range is the number of objects beyond the visible objects (plus and minus) that should be notified when they are close to being visible. For instance, if you have 3 objects on screen and a working range of 2, the previous and succeeding 2 objects will be notified that they are within the working range. As you scroll the list the range is updated as objects enter and exit the working range. To opt out of using the working range, use `initWithUpdater:viewController:` or provide a working range of `0`. */ - (instancetype)initWithUpdater:(id )updater viewController:(nullable UIViewController *)viewController workingRangeSize:(NSInteger)workingRangeSize NS_DESIGNATED_INITIALIZER; /** Initializes a new `IGListAdapter` object with a working range of `0`. @param updater An object that manages updates to the collection view. @param viewController The view controller that will house the adapter. @return A new list adapter object. */ - (instancetype)initWithUpdater:(id )updater viewController:(nullable UIViewController *)viewController; /** Perform an update from the previous state of the data source. This is analogous to calling `-[UICollectionView performBatchUpdates:completion:]`. @param animated A flag indicating if the transition should be animated. @param completion The block to execute when the updates complete. */ - (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion NS_SWIFT_DISABLE_ASYNC; /** Perform an immediate reload of the data in the data source, discarding the old objects. @param completion The block to execute when the reload completes. @warning Do not use this method to update without animations as it can be very expensive to teardown and rebuild all section controllers. Use `-[IGListAdapter performUpdatesAnimated:completion]` instead. */ - (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion NS_SWIFT_DISABLE_ASYNC; /** Reload the list for only the specified objects. @param objects The objects to reload. */ - (void)reloadObjects:(NSArray *)objects; /** Query the section controller at a given section index. Constant time lookup. @param section A section in the list. @return A section controller or `nil` if the section does not exist. */ - (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section; /** Query the section index of a list. Constant time lookup. @param sectionController A list object. @return The section index of the list if it exists, otherwise `NSNotFound`. */ - (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController; /** Returns the section controller for the specified object. Constant time lookup. @param object An object from the data source. @return A section controller or `nil` if `object` is not in the list. @see `-[IGListAdapterDataSource listAdapter:sectionControllerForObject:]` */ - (__kindof IGListSectionController * _Nullable)sectionControllerForObject:(id)object; /** Returns the object corresponding to the specified section controller in the list. Constant time lookup. @param sectionController A section controller in the list. @return The object for the specified section controller, or `nil` if not found. */ - (nullable id)objectForSectionController:(IGListSectionController *)sectionController; /** Returns the object corresponding to a section in the list. Constant time lookup. @param section A section in the list. @return The object for the specified section, or `nil` if the section does not exist. */ - (nullable id)objectAtSection:(NSInteger)section; /** Returns the section corresponding to the specified object in the list. Constant time lookup. @param object An object in the list. @return The section index of `object` if found, otherwise `NSNotFound`. */ - (NSInteger)sectionForObject:(id)object; /** Returns a copy of all the objects currently driving the adapter. @return An array of objects. */ - (NSArray *)objects; /** An unordered array of the currently visible section controllers. @return An array of section controllers. */ - (NSArray *)visibleSectionControllers; /** An unordered array of the currently visible objects. @return An array of objects */ - (NSArray *)visibleObjects; /** Less performant that `visibleObjects` but gives you an ordering to the list of visible objects in `self.objects`. @return An index set for objects in `self.objects`. Result's `.count` will be `0` if no visible objects. */ - (NSIndexSet *)indexesOfVisibleObjects; /** An unordered array of the currently visible cells for a given object. @param object An object in the list @return An array of collection view cells. */ - (NSArray *)visibleCellsForObject:(id)object; /** Scrolls to the specified object in the list adapter. @param object The object to which to scroll. @param supplementaryKinds The types of supplementary views in the section. @param scrollDirection An option indicating the direction to scroll. @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. @param additionalOffset Additional offset amount from the scroll position. @param animated A flag indicating if the scrolling should be animated. @note The additional offset amount is to shift the final scroll position by some horizontal or vertical amount depending on the scroll direction. This is necessary when scrolling to an object on a view with sticky headers, since the sticky header would otherwise cover the top portion of the object. */ - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection scrollPosition:(UICollectionViewScrollPosition)scrollPosition additionalOffset:(CGFloat)additionalOffset animated:(BOOL)animated; /** Returns the index path for the first visible cell that has been scrolled to. This refers to the cell currently at the top/left (0, 0) of the collection view's frame, inset by the collection view's contentInset top or left value if defined. @return The index path of the cell or nil if not found. */ - (nullable NSIndexPath *)indexPathForFirstVisibleItem; /** Gets the scroll offset of the first visible cell scrolled into in the collection view. This refers to the cell currently at the top/left (0, 0) of the collection view's frame, inset by the collection view's contentInset top or left value if defined. @param scrollDirection An option indicating the direction to scroll. @return additionalOffset is the offset amount the first visible cell is shifted from the start of the cell, the amount it has been scrolled into in the coordinates of the cell's bounds. */ - (CGFloat)offsetForFirstVisibleItemWithScrollDirection:(UICollectionViewScrollDirection)scrollDirection; /** Returns the size of a cell at the specified index path. @param indexPath The index path of the cell. å @return The size of the cell. */ - (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath; /** Returns the size of a supplementary view in the list at the specified index path. @param elementKind The kind of supplementary view. @param indexPath The index path of the supplementary view. @return The size of the supplementary view. */ - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath; /** Adds a listener to the list adapter. @param updateListener The object conforming to the `IGListAdapterUpdateListener` protocol. @note Listeners are held weakly so there is no need to call `-[IGListAdapter removeUpdateListener:]` on `dealloc`. */ - (void)addUpdateListener:(id)updateListener; /** Removes a listener from the list adapter. @param updateListener The object conforming to the `IGListAdapterUpdateListener` protocol. */ - (void)removeUpdateListener:(id)updateListener; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapter.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterInternal.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListAdapterUpdater.h" #import "IGListAdapterDelegateAnnouncer.h" #import "IGListArrayUtilsInternal.h" #import "IGListDebugger.h" #import "IGListDefaultExperiments.h" #import "IGListSectionControllerInternal.h" #import "IGListSupplementaryViewSource.h" #import "IGListTransitionData.h" #import "IGListUpdatingDelegate.h" #import "UICollectionViewLayout+InteractiveReordering.h" #import "UIScrollView+IGListKit.h" #import "UIViewController+IGListAdapterInternal.h" typedef struct OffsetRange { CGFloat min; CGFloat max; } OffsetRange; @implementation IGListAdapter { NSMapTable *_viewSectionControllerMap; // An array of blocks to execute once batch updates are finished NSMutableArray *_queuedCompletionBlocks; NSHashTable> *_updateListeners; } - (void)dealloc { [self.sectionMap reset]; } #pragma mark - Init - (instancetype)initWithUpdater:(id )updater viewController:(UIViewController *)viewController workingRangeSize:(NSInteger)workingRangeSize { IGAssertMainThread(); IGParameterAssert(updater); if (self = [super init]) { [UICollectionViewLayout setupInteractiveReordering]; NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions]; NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0]; _sectionMap = [[IGListSectionMap alloc] initWithMapTable:table]; _globalDelegateAnnouncer = [IGListAdapterDelegateAnnouncer sharedInstance]; _displayHandler = [IGListDisplayHandler new]; _workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize]; _updateListeners = [NSHashTable weakObjectsHashTable]; _viewSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory]; _updater = updater; _viewController = viewController; [viewController associateListAdapter:self]; _experiments = IGListDefaultExperiments(); [IGListDebugger trackAdapter:self]; } return self; } - (instancetype)initWithUpdater:(id)updater viewController:(UIViewController *)viewController { return [self initWithUpdater:updater viewController:viewController workingRangeSize:0]; } - (UICollectionView *)collectionView { return _collectionView; } - (void)setCollectionView:(UICollectionView *)collectionView { IGAssertMainThread(); // if collection view has been used by a different list adapter, treat it as if we were using a new collection view // this happens when embedding a UICollectionView inside a UICollectionViewCell that is reused if (_collectionView != collectionView || _collectionView.dataSource != self) { // if the collection view was being used with another IGListAdapter (e.g. cell reuse) // destroy the previous association so the old adapter doesn't update the wrong collection view static NSMapTable *globalCollectionViewAdapterMap = nil; if (globalCollectionViewAdapterMap == nil) { globalCollectionViewAdapterMap = [NSMapTable weakToWeakObjectsMapTable]; } [globalCollectionViewAdapterMap removeObjectForKey:_collectionView]; [[globalCollectionViewAdapterMap objectForKey:collectionView] setCollectionView:nil]; [globalCollectionViewAdapterMap setObject:self forKey:collectionView]; // dump old registered section controllers in the case that we are changing collection views or setting for // the first time _registeredCellIdentifiers = [NSMutableSet new]; _registeredNibNames = [NSMutableSet new]; _registeredSupplementaryViewIdentifiers = [NSMutableSet new]; _registeredSupplementaryViewNibNames = [NSMutableSet new]; // We can't just swap out the collectionView, because we might have on-going or pending updates. // `_updater` can take care of that by wrapping the change in `performDataSourceChange`. [_updater performDataSourceChange:^{ if (self->_collectionView.dataSource == self) { // Since we're not going to sync the previous collectionView anymore, lets not be its dataSource. self->_collectionView.dataSource = nil; } self->_collectionView = collectionView; self->_collectionView.dataSource = self; [self _updateCollectionViewDelegate]; // Sync the dataSource <> adapter for a couple of reasons: // 1. We might not have synced on -setDataSource, so now is the time to try again. // 2. Any in-flight `performUpdatesAnimated` were cancelled, so lets make sure we have the latest data. [self _updateObjects]; // The sync between the collectionView <> adapter will happen automically, since // we just changed the `collectionView.dataSource`. }]; if (@available(iOS 10.0, tvOS 10, *)) { _collectionView.prefetchingEnabled = NO; } [_collectionView.collectionViewLayout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self]; [_collectionView.collectionViewLayout invalidateLayout]; } } - (void)setDataSource:(id)dataSource { if (_dataSource == dataSource) { return; } [_updater performDataSourceChange:^{ self->_dataSource = dataSource; // Invalidate the collectionView internal section & item counts, as if its dataSource changed. self->_collectionView.dataSource = nil; self->_collectionView.dataSource = self; // Sync the dataSource <> adapter [self _updateObjects]; // The sync between the collectionView <> adapter will happen automically, since // we just changed the `collectionView.dataSource`. }]; } // reset and configure the delegate proxy whenever this property is set - (void)setCollectionViewDelegate:(id)collectionViewDelegate { IGAssertMainThread(); IGWarn(![collectionViewDelegate conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)], @"UICollectionViewDelegateFlowLayout conformance is automatically handled by IGListAdapter."); if (_collectionViewDelegate != collectionViewDelegate) { _collectionViewDelegate = collectionViewDelegate; [self _createProxyAndUpdateCollectionViewDelegate]; } } - (void)setScrollViewDelegate:(id)scrollViewDelegate { IGAssertMainThread(); if (_scrollViewDelegate != scrollViewDelegate) { _scrollViewDelegate = scrollViewDelegate; [self _createProxyAndUpdateCollectionViewDelegate]; } } - (void)_updateObjects { if (_collectionView == nil) { // If we don't have a collectionView, we can't do much. return; } id dataSource = _dataSource; NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]); [self _updateObjects:uniqueObjects dataSource:dataSource]; } - (void)_createProxyAndUpdateCollectionViewDelegate { // there is a known bug with accessibility and using an NSProxy as the delegate that will cause EXC_BAD_ACCESS // when voiceover is enabled. it will hold an unsafe ref to the delegate _collectionView.delegate = nil; self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegate scrollViewTarget:_scrollViewDelegate interceptor:self]; [self _updateCollectionViewDelegate]; } - (void)_updateCollectionViewDelegate { // set up the delegate to the proxy so the adapter can intercept events // default to the adapter simply being the delegate _collectionView.delegate = (id)self.delegateProxy ?: self; } #pragma mark - Scrolling - (void)scrollToObject:(id)object supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection scrollPosition:(UICollectionViewScrollPosition)scrollPosition additionalOffset:(CGFloat)additionalOffset animated:(BOOL)animated { IGAssertMainThread(); IGParameterAssert(object != nil); const NSInteger section = [self sectionForObject:object]; if (section == NSNotFound) { return; } UICollectionView *collectionView = self.collectionView; // We avoid calling `[collectionView layoutIfNeeded]` here because that could create cells that will no longer be visible after the scroll. // Note that we get the layout attributes from the `UICollectionView` instead of the `collectionViewLayout`, because that will generate the // necessary attributes without creating the cells just yet. NSIndexPath *indexPathFirstElement = [NSIndexPath indexPathForItem:0 inSection:section]; const OffsetRange offset = [self _offsetRangeForIndexPath:indexPathFirstElement supplementaryKinds:supplementaryKinds scrollDirection:scrollDirection]; const CGFloat offsetMid = (offset.min + offset.max) / 2.0; const CGFloat collectionViewWidth = collectionView.bounds.size.width; const CGFloat collectionViewHeight = collectionView.bounds.size.height; const UIEdgeInsets contentInset = collectionView.ig_contentInset; CGPoint contentOffset = collectionView.contentOffset; switch (scrollDirection) { case UICollectionViewScrollDirectionHorizontal: { switch (scrollPosition) { case UICollectionViewScrollPositionRight: contentOffset.x = offset.max - collectionViewWidth + contentInset.right; break; case UICollectionViewScrollPositionCenteredHorizontally: { const CGFloat insets = (contentInset.left - contentInset.right) / 2.0; contentOffset.x = offsetMid - collectionViewWidth / 2.0 - insets; } break; case UICollectionViewScrollPositionLeft: case UICollectionViewScrollPositionNone: case UICollectionViewScrollPositionTop: case UICollectionViewScrollPositionBottom: case UICollectionViewScrollPositionCenteredVertically: contentOffset.x = offset.min - contentInset.left; break; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollPosition, scrollPosition); } const CGFloat maxOffsetX = collectionView.contentSize.width - collectionView.frame.size.width + contentInset.right; const CGFloat minOffsetX = -contentInset.left; contentOffset.x += additionalOffset; contentOffset.x = MIN(contentOffset.x, maxOffsetX); contentOffset.x = MAX(contentOffset.x, minOffsetX); break; } case UICollectionViewScrollDirectionVertical: { switch (scrollPosition) { case UICollectionViewScrollPositionBottom: contentOffset.y = offset.max - collectionViewHeight + contentInset.bottom; break; case UICollectionViewScrollPositionCenteredVertically: { const CGFloat insets = (contentInset.top - contentInset.bottom) / 2.0; contentOffset.y = offsetMid - collectionViewHeight / 2.0 - insets; } break; case UICollectionViewScrollPositionTop: case UICollectionViewScrollPositionNone: case UICollectionViewScrollPositionLeft: case UICollectionViewScrollPositionRight: case UICollectionViewScrollPositionCenteredHorizontally: contentOffset.y = offset.min - contentInset.top; break; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollPosition, scrollPosition); } // If we don't call [collectionView layoutIfNeeded], the collectionView.contentSize does not get updated. // So lets use the layout object, since it should have been updated by now. const CGFloat maxHeight = collectionView.collectionViewLayout.collectionViewContentSize.height; const CGFloat maxOffsetY = maxHeight - collectionView.frame.size.height + contentInset.bottom; const CGFloat minOffsetY = -contentInset.top; contentOffset.y += additionalOffset; contentOffset.y = MIN(contentOffset.y, maxOffsetY); contentOffset.y = MAX(contentOffset.y, minOffsetY); break; } default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, scrollDirection); } [collectionView setContentOffset:contentOffset animated:animated]; } - (nullable NSIndexPath *)indexPathForFirstVisibleItem { const CGPoint contentOffset = self.collectionView.contentOffset; const UIEdgeInsets contentInset = self.collectionView.contentInset; const CGPoint point = CGPointMake(contentOffset.x + contentInset.left, contentOffset.y + contentInset.top); return [self.collectionView indexPathForItemAtPoint:point]; } - (CGFloat)offsetForFirstVisibleItemWithScrollDirection:(UICollectionViewScrollDirection)scrollDirection { NSIndexPath *const indexPath = [self indexPathForFirstVisibleItem]; if (indexPath) { const OffsetRange offset = [self _offsetRangeForIndexPath:indexPath supplementaryKinds:nil scrollDirection:scrollDirection]; switch (scrollDirection) { case UICollectionViewScrollDirectionHorizontal: return self.collectionView.contentInset.left + self.collectionView.contentOffset.x - offset.min; case UICollectionViewScrollDirectionVertical: return self.collectionView.contentInset.top + self.collectionView.contentOffset.y - offset.min; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, scrollDirection); } } else { return 0; } } - (OffsetRange)_offsetRangeForIndexPath:(NSIndexPath *)indexPath supplementaryKinds:(nullable NSArray *)supplementaryKinds scrollDirection:(UICollectionViewScrollDirection)scrollDirection { const NSUInteger section = indexPath.section; // collect the layout attributes for the cell and supplementary views for the first index // this will break if there are supplementary views beyond item 0 NSMutableArray *attributes = nil; const NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:section]; if (numberOfItems > 0) { attributes = [self _layoutAttributesForItemAndSupplementaryViewAtIndexPath:indexPath supplementaryKinds:supplementaryKinds].mutableCopy; if (numberOfItems > 1) { NSIndexPath *indexPathLastElement = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:section]; UICollectionViewLayoutAttributes *lastElementattributes = [self _layoutAttributesForItemAndSupplementaryViewAtIndexPath:indexPathLastElement supplementaryKinds:supplementaryKinds].firstObject; if (lastElementattributes != nil) { [attributes addObject:lastElementattributes]; } } } else { NSMutableArray *supplementaryAttributes = [NSMutableArray new]; for (NSString* supplementaryKind in supplementaryKinds) { UICollectionViewLayoutAttributes *supplementaryAttribute = [self _layoutAttributesForSupplementaryViewOfKind:supplementaryKind atIndexPath:indexPath]; if (supplementaryAttribute != nil) { [supplementaryAttributes addObject: supplementaryAttribute]; } } attributes = supplementaryAttributes; } OffsetRange offset = (OffsetRange) { .min = 0, .max = 0 }; for (UICollectionViewLayoutAttributes *attribute in attributes) { const CGRect frame = attribute.frame; CGFloat originMin; CGFloat endMax; switch (scrollDirection) { case UICollectionViewScrollDirectionHorizontal: originMin = CGRectGetMinX(frame); endMax = CGRectGetMaxX(frame); break; case UICollectionViewScrollDirectionVertical: originMin = CGRectGetMinY(frame); endMax = CGRectGetMaxY(frame); break; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, scrollDirection); } // find the minimum origin value of all the layout attributes if (attribute == attributes.firstObject || originMin < offset.min) { offset.min = originMin; } // find the maximum end value of all the layout attributes if (attribute == attributes.firstObject || endMax > offset.max) { offset.max = endMax; } } return offset; } #pragma mark - Editing - (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletion)completion { IGAssertMainThread(); id dataSource = self.dataSource; id updater = self.updater; UICollectionView *collectionView = self.collectionView; if (dataSource == nil || collectionView == nil) { IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__); IGLK_BLOCK_CALL_SAFE(completion, NO); return; } [self _enterBatchUpdates]; __weak __typeof__(self) weakSelf = self; IGListTransitionDataBlock sectionDataBlock = ^IGListTransitionData *{ __typeof__(self) strongSelf = weakSelf; IGListTransitionData *transitionData = nil; if (strongSelf) { NSArray *toObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:strongSelf]); transitionData = [strongSelf _generateTransitionDataWithObjects:toObjects dataSource:dataSource]; } return transitionData; }; IGListTransitionDataApplyBlock applySectionDataBlock = ^void(IGListTransitionData *data) { __typeof__(self) strongSelf = weakSelf; if (strongSelf) { // temporarily capture the item map that we are transitioning from in case // there are any item deletes at the same strongSelf.previousSectionMap = [strongSelf.sectionMap copy]; [strongSelf _updateWithData:data]; } }; IGListUpdaterCompletion outerCompletionBlock = ^(BOOL finished){ __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { IGLK_BLOCK_CALL_SAFE(completion,finished); return; } // release the previous items strongSelf.previousSectionMap = nil; [strongSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; IGLK_BLOCK_CALL_SAFE(completion,finished); [strongSelf _exitBatchUpdates]; }; [updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] animated:animated sectionDataBlock:sectionDataBlock applySectionDataBlock:applySectionDataBlock completion:outerCompletionBlock]; } - (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion { IGAssertMainThread(); id dataSource = self.dataSource; UICollectionView *collectionView = self.collectionView; if (dataSource == nil || collectionView == nil) { IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__); if (completion) { completion(NO); } return; } NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]); __weak __typeof__(self) weakSelf = self; [self.updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock] reloadUpdateBlock:^{ // purge all section controllers from the item map so that they are regenerated [weakSelf.sectionMap reset]; [weakSelf _updateObjects:uniqueObjects dataSource:dataSource]; } completion:^(BOOL finished) { [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO]; if (completion) { completion(finished); } }]; } - (void)reloadObjects:(NSArray *)objects { IGAssertMainThread(); IGParameterAssert(objects); NSMutableIndexSet *sections = [NSMutableIndexSet new]; // use the item map based on whether or not we're in an update block IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:YES]; [objects enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) { // look up the item using the map's lookup function. might not be the same item const NSInteger section = [map sectionForObject:object]; const BOOL notFound = section == NSNotFound; if (notFound) { return; } [sections addIndex:section]; // reverse lookup the item using the section. if the pointer has changed the trigger update events and swap items if (object != [map objectForSection:section]) { [map updateObject:object]; [[map sectionControllerForSection:section] didUpdateToObject:object]; } }]; UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Tried to reload the adapter without a collection view"); [self.updater reloadCollectionView:collectionView sections:sections]; } - (void)addUpdateListener:(id)updateListener { IGAssertMainThread(); IGParameterAssert(updateListener != nil); [_updateListeners addObject:updateListener]; } - (void)removeUpdateListener:(id)updateListener { IGAssertMainThread(); IGParameterAssert(updateListener != nil); [_updateListeners removeObject:updateListener]; } - (void)_notifyDidUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated { for (id listener in _updateListeners) { [listener listAdapter:self didFinishUpdate:update animated:animated]; } } #pragma mark - List Items & Sections - (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section { IGAssertMainThread(); return [self.sectionMap sectionControllerForSection:section]; } - (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(sectionController != nil); return [self.sectionMap sectionForSectionController:sectionController]; } - (IGListSectionController *)sectionControllerForObject:(id)object { IGAssertMainThread(); IGParameterAssert(object != nil); return [self.sectionMap sectionControllerForObject:object]; } - (id)objectForSectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(sectionController != nil); const NSInteger section = [self.sectionMap sectionForSectionController:sectionController]; return [self.sectionMap objectForSection:section]; } - (id)objectAtSection:(NSInteger)section { IGAssertMainThread(); return [self.sectionMap objectForSection:section]; } - (NSInteger)sectionForObject:(id)item { IGAssertMainThread(); IGParameterAssert(item != nil); return [self.sectionMap sectionForObject:item]; } - (NSArray *)objects { IGAssertMainThread(); return self.sectionMap.objects; } - (id)_supplementaryViewSourceAtIndexPath:(NSIndexPath *)indexPath { IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; return [sectionController supplementaryViewSource]; } - (NSArray *)visibleSectionControllers { IGAssertMainThread(); return [[self.displayHandler visibleListSections] allObjects]; } - (NSSet *)_visibleObjectsSet __attribute__((objc_direct)) { IGAssertMainThread(); NSArray *visibleCells = [self.collectionView visibleCells]; NSMutableSet *visibleObjects = [NSMutableSet new]; for (UICollectionViewCell *cell in visibleCells) { IGListSectionController *sectionController = [self _sectionControllerForCell:cell]; IGAssert(sectionController != nil, @"Section controller nil for cell %@", cell); if (sectionController != nil) { const NSInteger section = [self sectionForSectionController:sectionController]; if (section != NSNotFound) { id object = [self objectAtSection:section]; IGAssert(object != nil, @"Object not found for section controller %@ at section %li", sectionController, (long)section); if (object != nil) { [visibleObjects addObject:object]; } } } } return visibleObjects; } - (NSArray *)visibleObjects { return [[self _visibleObjectsSet] allObjects]; } - (NSIndexSet *)indexesOfVisibleObjects { /* This is a naive implementation, going through all objects and checking if they are visible. It is not optimized for performance, but it is correct. In the future, this could potentially be optimized by getting the index paths of visible cells, and converting those index paths into a range of object indexes within `self.objects`. */ NSSet *visibleObjects = [self _visibleObjectsSet]; NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet]; NSUInteger idx = 0; for (id object in self.objects) { if ([visibleObjects containsObject:object]) { [indexSet addIndex:idx]; } idx++; } return [indexSet copy]; } - (NSArray *)visibleCellsForObject:(id)object { IGAssertMainThread(); IGParameterAssert(object != nil); const NSInteger section = [self.sectionMap sectionForObject:object]; if (section == NSNotFound) { return [NSArray new]; } NSArray *visibleCells = [self.collectionView visibleCells]; UICollectionView *collectionView = self.collectionView; NSPredicate *controllerPredicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewCell* cell, NSDictionary* bindings) { NSIndexPath *indexPath = [collectionView indexPathForCell:cell]; return indexPath.section == section; }]; return [visibleCells filteredArrayUsingPredicate:controllerPredicate]; } #pragma mark - Layout - (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); id performanceDelegate = self.performanceDelegate; [performanceDelegate listAdapterWillCallSize:self]; IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; const CGSize size = [sectionController sizeForItemAtIndex:indexPath.item]; const CGSize positiveSize = CGSizeMake(MAX(size.width, 0.0), MAX(size.height, 0.0)); [performanceDelegate listAdapter:self didCallSizeOnSectionController:sectionController atIndex:indexPath.item]; return positiveSize; } - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); id supplementaryViewSource = [self _supplementaryViewSourceAtIndexPath:indexPath]; if ([[supplementaryViewSource supportedElementKinds] containsObject:elementKind]) { const CGSize size = [supplementaryViewSource sizeForSupplementaryViewOfKind:elementKind atIndex:indexPath.item]; return CGSizeMake(MAX(size.width, 0.0), MAX(size.height, 0.0)); } return CGSizeZero; } #pragma mark - Private API - (IGListCollectionViewBlock)_collectionViewBlock { __weak __typeof__(self) weakSelf = self; return ^UICollectionView *{ return weakSelf.collectionView; }; } - (IGListTransitionData *)_generateTransitionDataWithObjects:(NSArray *)objects dataSource:(id)dataSource { IGListSectionMap *map = self.sectionMap; if (!dataSource) { return [[IGListTransitionData alloc] initFromObjects:map.objects toObjects:@[] toSectionControllers:@[]]; } #if defined(DEBUG) && DEBUG for (id object in objects) { IGAssert([object isEqualToDiffableObject:object], @"Object instance %@ not equal to itself. This will break infra map tables.", object); } #endif NSMutableArray *sectionControllers = [[NSMutableArray alloc] initWithCapacity:objects.count]; NSMutableArray *validObjects = [[NSMutableArray alloc] initWithCapacity:objects.count]; // push the view controller and collection context into a local thread container so they are available on init // for IGListSectionController subclasses after calling [super init] IGListSectionControllerPushThread(self.viewController, self); [objects enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) { // infra checks to see if a controller exists IGListSectionController *sectionController = [map sectionControllerForObject:object]; // if not, query the data source for a new one if (sectionController == nil) { sectionController = [dataSource listAdapter:self sectionControllerForObject:object]; } if (sectionController == nil) { IGLKLog(@"WARNING: Ignoring nil section controller returned by data source %@ for object %@.", dataSource, object); return; } if ([sectionController isMemberOfClass:[IGListSectionController class]]) { // If IGListSectionController is not subclassed, it could be a side effect of a problem. For example, nothing stops // dataSource from returning a plain IGListSectionController if it doesn't recognize the object type, instead of throwing. // Why not throw here then? Maybe we should, but in most cases, it feels like an over reaction. If we don't know how to render // a single item, terminating the entire app might not be necessary. The dataSource should be the one who decides if throwing is appropriate. IGFailAssert(@"Ignoring IGListSectionController that's not a subclass from data source %@ for object %@", NSStringFromClass([dataSource class]), NSStringFromClass([object class])); return; } // in case the section controller was created outside of -listAdapter:sectionControllerForObject: sectionController.collectionContext = self; sectionController.viewController = self.viewController; [sectionControllers addObject:sectionController]; [validObjects addObject:object]; }]; #if defined(DEBUG) && DEBUG IGAssert([NSSet setWithArray:sectionControllers].count == sectionControllers.count, @"Section controllers array is not filled with unique objects; section controllers are being reused"); #endif // clear the view controller and collection context IGListSectionControllerPopThread(); return [[IGListTransitionData alloc] initFromObjects:map.objects toObjects:validObjects toSectionControllers:sectionControllers]; } - (void)_updateObjects:(NSArray *)objects dataSource:(id)dataSource { [self _updateWithData:[self _generateTransitionDataWithObjects:objects dataSource:dataSource]]; } // this method is what updates the "source of truth" // this should only be called just before the collection view is updated - (void)_updateWithData:(IGListTransitionData *)data { IGParameterAssert(data); // Should be the first thing called in this function. _isInObjectUpdateTransaction = YES; IGListSectionMap *map = self.sectionMap; // Note: We use an array, instead of a set, because the updater should have dealt with duplicates already. NSMutableArray *updatedObjects = [NSMutableArray new]; for (id object in data.toObjects) { // check if the item has changed instances or is new const NSInteger oldSection = [map sectionForObject:object]; if (oldSection == NSNotFound || [map objectForSection:oldSection] != object) { [updatedObjects addObject:object]; } } [map updateWithObjects:data.toObjects sectionControllers:data.toSectionControllers]; // now that the maps have been created and contexts are assigned, we consider the section controller "fully loaded" for (id object in updatedObjects) { [[map sectionControllerForObject:object] didUpdateToObject:object]; } [self _updateBackgroundView]; // Should be the last thing called in this function. _isInObjectUpdateTransaction = NO; } - (void)_updateBackgroundView { const BOOL shouldDisplay = [self _itemCountIsZero]; if (shouldDisplay) { UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self]; // don't do anything if the client is using the same view if (backgroundView != _collectionView.backgroundView) { // collection view will just stack the background views underneath each other if we do not remove the previous // one first. also fine if it is nil [_collectionView.backgroundView removeFromSuperview]; _collectionView.backgroundView = backgroundView; } } _collectionView.backgroundView.hidden = !shouldDisplay; } - (BOOL)_itemCountIsZero { __block BOOL isZero = YES; [self.sectionMap enumerateUsingBlock:^(id _Nonnull object, IGListSectionController * _Nonnull sectionController, NSInteger section, BOOL * _Nonnull stop) { if (sectionController.numberOfItems > 0) { isZero = NO; *stop = YES; } }]; return isZero; } - (IGListSectionMap *)_sectionMapUsingPreviousIfInUpdateBlock:(BOOL)usePreviousMapIfInUpdateBlock { // if we are inside an update block, we may have to use the /previous/ item map for some operations IGListSectionMap *previousSectionMap = self.previousSectionMap; if (usePreviousMapIfInUpdateBlock && [self isInDataUpdateBlock] && previousSectionMap != nil) { return previousSectionMap; } else { return self.sectionMap; } } - (NSArray *)indexPathsFromSectionController:(IGListSectionController *)sectionController indexes:(NSIndexSet *)indexes usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock { NSMutableArray *indexPaths = [NSMutableArray new]; IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:usePreviousIfInUpdateBlock]; const NSInteger section = [map sectionForSectionController:sectionController]; if (section != NSNotFound) { [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; }]; } return indexPaths; } - (NSIndexPath *)indexPathForSectionController:(IGListSectionController *)controller index:(NSInteger)index usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock { IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:usePreviousIfInUpdateBlock]; const NSInteger section = [map sectionForSectionController:controller]; if (section == NSNotFound) { return nil; } else { return [NSIndexPath indexPathForItem:index inSection:section]; } } - (NSArray *)_layoutAttributesForItemAndSupplementaryViewAtIndexPath:(NSIndexPath *)indexPath supplementaryKinds:(NSArray *)supplementaryKinds { NSMutableArray *attributes = [NSMutableArray new]; UICollectionViewLayoutAttributes *cellAttributes = [self _layoutAttributesForItemAtIndexPath:indexPath]; if (cellAttributes) { [attributes addObject:cellAttributes]; } for (NSString *kind in supplementaryKinds) { UICollectionViewLayoutAttributes *supplementaryAttributes = [self _layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath]; if (supplementaryAttributes) { [attributes addObject:supplementaryAttributes]; } } return attributes; } - (nullable UICollectionViewLayoutAttributes *)_layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return [self.collectionView layoutAttributesForItemAtIndexPath:indexPath]; } - (nullable UICollectionViewLayoutAttributes *)_layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { return [self.collectionView layoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath]; } - (void)mapView:(UICollectionReusableView *)view toSectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(view != nil); IGParameterAssert(sectionController != nil); [_viewSectionControllerMap setObject:sectionController forKey:view]; } - (nullable IGListSectionController *)sectionControllerForView:(UICollectionReusableView *)view { IGAssertMainThread(); return [_viewSectionControllerMap objectForKey:view]; } - (nullable IGListSectionController *)_sectionControllerForCell:(UICollectionViewCell *)cell { IGAssertMainThread(); return [_viewSectionControllerMap objectForKey:cell]; } - (void)removeMapForView:(UICollectionReusableView *)view { IGAssertMainThread(); [_viewSectionControllerMap removeObjectForKey:view]; } - (void)_deferBlockBetweenBatchUpdates:(void (^)(void))block { IGAssertMainThread(); if (_queuedCompletionBlocks == nil) { block(); } else { [_queuedCompletionBlocks addObject:block]; } } - (void)_enterBatchUpdates { _queuedCompletionBlocks = [NSMutableArray new]; } - (void)_exitBatchUpdates { NSArray *blocks = [_queuedCompletionBlocks copy]; _queuedCompletionBlocks = nil; for (void (^block)(void) in blocks) { block(); } } - (BOOL)isInDataUpdateBlock { return self.updater.isInDataUpdateBlock; } #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { id performanceDelegate = self.performanceDelegate; [performanceDelegate listAdapterWillCallScroll:self]; // forward this method to the delegate b/c this implementation will steal the message from the proxy id scrollViewDelegate = self.scrollViewDelegate; if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { [scrollViewDelegate scrollViewDidScroll:scrollView]; } NSArray *visibleSectionControllers = [self visibleSectionControllers]; for (IGListSectionController *sectionController in visibleSectionControllers) { [[sectionController scrollDelegate] listAdapter:self didScrollSectionController:sectionController]; } [performanceDelegate listAdapter:self didCallScroll:scrollView]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // forward this method to the delegate b/c this implementation will steal the message from the proxy id scrollViewDelegate = self.scrollViewDelegate; if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) { [scrollViewDelegate scrollViewWillBeginDragging:scrollView]; } NSArray *visibleSectionControllers = [self visibleSectionControllers]; for (IGListSectionController *sectionController in visibleSectionControllers) { [[sectionController scrollDelegate] listAdapter:self willBeginDraggingSectionController:sectionController]; } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { // forward this method to the delegate b/c this implementation will steal the message from the proxy id scrollViewDelegate = self.scrollViewDelegate; if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) { [scrollViewDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; } NSArray *visibleSectionControllers = [self visibleSectionControllers]; for (IGListSectionController *sectionController in visibleSectionControllers) { [[sectionController scrollDelegate] listAdapter:self didEndDraggingSectionController:sectionController willDecelerate:decelerate]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { // forward this method to the delegate b/c this implementation will steal the message from the proxy id scrollViewDelegate = self.scrollViewDelegate; if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) { [scrollViewDelegate scrollViewDidEndDecelerating:scrollView]; } NSArray *visibleSectionControllers = [self visibleSectionControllers]; for (IGListSectionController *sectionController in visibleSectionControllers) { id scrollDelegate = [sectionController scrollDelegate]; if ([scrollDelegate respondsToSelector:@selector(listAdapter:didEndDeceleratingSectionController:)]) { [scrollDelegate listAdapter:self didEndDeceleratingSectionController:sectionController]; } } } #pragma mark - IGListCollectionContext - (CGSize)containerSize { return self.collectionView.bounds.size; } - (UITraitCollection *)traitCollection { return self.collectionView.traitCollection; } - (UIEdgeInsets)containerInset { return self.collectionView.contentInset; } - (UIEdgeInsets)adjustedContainerInset { return self.collectionView.ig_contentInset; } - (CGSize)insetContainerSize { UICollectionView *collectionView = self.collectionView; return UIEdgeInsetsInsetRect(collectionView.bounds, collectionView.ig_contentInset).size; } - (CGPoint)containerContentOffset { return self.collectionView.contentOffset; } - (IGListCollectionScrollingTraits)scrollingTraits { UICollectionView *collectionView = self.collectionView; return (IGListCollectionScrollingTraits) { .isTracking = collectionView.isTracking, .isDragging = collectionView.isDragging, .isDecelerating = collectionView.isDecelerating, }; } - (CGSize)containerSizeForSectionController:(IGListSectionController *)sectionController { const UIEdgeInsets inset = sectionController.inset; return CGSizeMake(self.containerSize.width - inset.left - inset.right, self.containerSize.height - inset.top - inset.bottom); } - (NSInteger)indexForCell:(UICollectionViewCell *)cell sectionController:(nonnull IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(cell != nil); IGParameterAssert(sectionController != nil); NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; IGAssert(indexPath == nil || indexPath.section == [self sectionForSectionController:sectionController], @"Requesting a cell from another section controller is not allowed."); return indexPath != nil ? indexPath.item : NSNotFound; } - (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(sectionController != nil); // if this is accessed while a cell is being dequeued or displaying working range elements, just return nil if (_isDequeuingCell || _isSendingWorkingRangeDisplayUpdates) { return nil; } NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:YES]; // prevent querying the collection view if it isn't fully reloaded yet for the current data set if (indexPath != nil && indexPath.section < [self.collectionView numberOfSections]) { // only return a cell if it belongs to the section controller // this association is created in -collectionView:cellForItemAtIndexPath: UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath]; if ([self _sectionControllerForCell:cell] == sectionController) { return cell; } } return nil; } - (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(sectionController != nil); // if this is accessed while a cell is being dequeued or displaying working range elements, just return nil if (_isDequeuingSupplementaryView || _isSendingWorkingRangeDisplayUpdates) { return nil; } NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:YES]; // prevent querying the collection view if it isn't fully reloaded yet for the current data set if (indexPath != nil && indexPath.section < [self.collectionView numberOfSections]) { // only return a supplementary view if it belongs to the section controller UICollectionReusableView *view = [self.collectionView supplementaryViewForElementKind:elementKind atIndexPath:indexPath]; if ([self sectionControllerForView:view] == sectionController) { return view; } } return nil; } - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController { NSIndexPath *const indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; return [_collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath]; } - (NSArray *)fullyVisibleCellsForSectionController:(IGListSectionController *)sectionController { const NSInteger section = [self sectionForSectionController:sectionController]; if (section == NSNotFound) { // The section controller is not in the map, which can happen if the associated object was deleted or after a full reload. return @[]; } NSMutableArray *cells = [NSMutableArray new]; UICollectionView *collectionView = self.collectionView; NSArray *visibleCells = [collectionView visibleCells]; for (UICollectionViewCell *cell in visibleCells) { if ([collectionView indexPathForCell:cell].section == section) { const CGRect cellRect = [cell convertRect:cell.bounds toView:collectionView]; if (CGRectContainsRect(UIEdgeInsetsInsetRect(collectionView.bounds, collectionView.contentInset), cellRect)) { [cells addObject:cell]; } } } return cells; } - (NSArray *)visibleCellsForSectionController:(IGListSectionController *)sectionController { const NSInteger section = [self sectionForSectionController:sectionController]; if (section == NSNotFound) { // The section controller is not in the map, which can happen if the associated object was deleted or after a full reload. return @[]; } NSMutableArray *cells = [NSMutableArray new]; UICollectionView *collectionView = self.collectionView; NSArray *visibleCells = [collectionView visibleCells]; for (UICollectionViewCell *cell in visibleCells) { if ([collectionView indexPathForCell:cell].section == section) { [cells addObject:cell]; } } return cells; } - (NSArray *)visibleIndexPathsForSectionController:(IGListSectionController *) sectionController { const NSInteger section = [self sectionForSectionController:sectionController]; if (section == NSNotFound) { // The section controller is not in the map, which can happen if the associated object was deleted or after a full reload. return @[]; } NSMutableArray *paths = [NSMutableArray new]; UICollectionView *collectionView = self.collectionView; NSArray *visiblePaths = [collectionView indexPathsForVisibleItems]; for (NSIndexPath *path in visiblePaths) { if (path.section == section) { [paths addObject:path]; } } return paths; } - (void)deselectItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController animated:(BOOL)animated { IGAssertMainThread(); IGParameterAssert(sectionController != nil); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; [self.collectionView deselectItemAtIndexPath:indexPath animated:animated]; } - (void)selectItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition { IGAssertMainThread(); IGParameterAssert(sectionController != nil); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; [self.collectionView selectItemAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition]; } - (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass withReuseIdentifier:(NSString *)reuseIdentifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert(sectionController != nil); IGParameterAssert(cellClass != nil); IGParameterAssert(index >= 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Dequeueing cell of class %@ with reuseIdentifier %@ from section controller %@ without a collection view at index %li", NSStringFromClass(cellClass), reuseIdentifier, sectionController, (long)index); NSString *identifier = IGListReusableViewIdentifier(cellClass, nil, reuseIdentifier); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; if (![self.registeredCellIdentifiers containsObject:identifier]) { [self.registeredCellIdentifiers addObject:identifier]; [collectionView registerClass:cellClass forCellWithReuseIdentifier:identifier]; } return [self _dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath forSectionController:sectionController]; } - (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { return [self dequeueReusableCellOfClass:cellClass withReuseIdentifier:nil forSectionController:sectionController atIndex:index]; } - (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert(sectionController != nil); IGParameterAssert(identifier.length > 0); IGAssert(self.collectionView != nil, @"Reloading adapter without a collection view."); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; return [self _dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath forSectionController:sectionController]; } - (UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert([nibName length] > 0); IGParameterAssert(sectionController != nil); IGParameterAssert(index >= 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Dequeueing cell with nib name %@ and bundle %@ from section controller %@ without a collection view at index %li.", nibName, bundle, sectionController, (long)index); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; if (![self.registeredNibNames containsObject:nibName]) { [self.registeredNibNames addObject:nibName]; UINib *nib = [UINib nibWithNibName:nibName bundle:bundle]; [collectionView registerNib:nib forCellWithReuseIdentifier:nibName]; } return [self _dequeueReusableCellWithReuseIdentifier:nibName forIndexPath:indexPath forSectionController:sectionController]; } - (UICollectionViewCell *)_dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath forSectionController:(IGListSectionController *)sectionController { // These will cause a crash in iOS 18 IGAssert(_dequeuedCells.count == 0, @"Dequeueing more than one cell (%@) for indexPath %@, section controller %@,", identifier, indexPath, sectionController); IGAssert(_isDequeuingCell, @"Dequeueing a cell (%@) without a request from the UICollectionView for indexPath %@, section controller %@", identifier, indexPath, sectionController); UICollectionViewCell *const cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; if (_isDequeuingCell && cell) { [_dequeuedCells addObject:cell]; } return cell; } - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind forSectionController:(IGListSectionController *)sectionController class:(Class)viewClass atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert(elementKind.length > 0); IGParameterAssert(sectionController != nil); IGParameterAssert(viewClass != nil); IGParameterAssert(index >= 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Dequeueing cell of class %@ from section controller %@ without a collection view at index %li with supplementary view %@", NSStringFromClass(viewClass), sectionController, (long)index, elementKind); NSString *identifier = IGListReusableViewIdentifier(viewClass, elementKind, nil); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; if (![self.registeredSupplementaryViewIdentifiers containsObject:identifier]) { [self.registeredSupplementaryViewIdentifiers addObject:identifier]; [collectionView registerClass:viewClass forSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier]; } return [self _dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath forSectionController:sectionController]; } - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind withIdentifier:(NSString *)identifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert(elementKind.length > 0); IGParameterAssert(identifier.length > 0); IGParameterAssert(sectionController != nil); IGParameterAssert(index >= 0); IGAssert(self.collectionView != nil, @"Dequeueing Supplementary View from storyboard of kind %@ with identifier %@ for section controller %@ without a collection view at index %li", elementKind, identifier, sectionController, (long)index); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; return [self _dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath forSectionController:sectionController]; } - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind forSectionController:(IGListSectionController *)sectionController nibName:(NSString *)nibName bundle:(NSBundle *)bundle atIndex:(NSInteger)index { IGAssertMainThread(); IGParameterAssert([nibName length] > 0); IGParameterAssert([elementKind length] > 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Reloading adapter without a collection view."); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; if (![self.registeredSupplementaryViewNibNames containsObject:nibName]) { [self.registeredSupplementaryViewNibNames addObject:nibName]; UINib *nib = [UINib nibWithNibName:nibName bundle:bundle]; [collectionView registerNib:nib forSupplementaryViewOfKind:elementKind withReuseIdentifier:nibName]; } return [self _dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:nibName forIndexPath:indexPath forSectionController:sectionController]; } - (__kindof UICollectionReusableView *)_dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind withReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath forSectionController:(IGListSectionController *)sectionController { // These will cause a crash in iOS 18 IGAssert(_dequeuedSupplementaryViews.count == 0, @"Dequeueing more than one supplementary-view (%@) for indexPath %@, section controller %@,", identifier, indexPath, sectionController); IGAssert(_isDequeuingSupplementaryView, @"Dequeueing a supplementary-view (%@) without a request from the UICollectionView for indexPath %@, section controller %@", identifier, indexPath, sectionController); UICollectionReusableView *const view = [self.collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath]; if (_isDequeuingSupplementaryView && view) { [_dequeuedSupplementaryViews addObject:view]; } return view; } - (void)performBatchAnimated:(BOOL)animated updates:(void (^)(id))updates completion:(void (^)(BOOL))completion { IGAssertMainThread(); IGParameterAssert(updates != nil); IGWarn(self.collectionView != nil, @"Performing batch updates without a collection view."); [self _enterBatchUpdates]; __weak __typeof__(self) weakSelf = self; [self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] animated:animated itemUpdates:^{ // the adapter acts as the batch context with its API stripped to just the IGListBatchContext protocol updates(weakSelf); } completion: ^(BOOL finished) { [weakSelf _updateBackgroundView]; [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeItemUpdates animated:animated]; if (completion) { completion(finished); } [weakSelf _exitBatchUpdates]; }]; } - (void)scrollToSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index scrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated { IGAssertMainThread(); IGParameterAssert(sectionController != nil); NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; } - (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController completion:(void (^)(BOOL finished))completion { __weak __typeof__(self) weakSelf = self; // do not call -[UICollectionView performBatchUpdates:completion:] while already updating. defer it until completed. [self _deferBlockBetweenBatchUpdates:^{ // Note that we calculate the `NSIndexPaths` after the batch update, otherwise they're be wrong. [weakSelf _invalidateLayoutForSectionController:sectionController completion:completion]; }]; } - (void)_invalidateLayoutForSectionController:(IGListSectionController *)sectionController completion:(void (^)(BOOL finished))completion { const NSInteger section = [self sectionForSectionController:sectionController]; if (section == NSNotFound) { // The section controller is not in the map, which can happen if the associated object was deleted or after a full reload. if (completion) { completion(NO); } return; } const NSInteger items = [_collectionView numberOfItemsInSection:section]; NSMutableArray *indexPaths = [NSMutableArray new]; for (NSInteger item = 0; item < items; item++) { [indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]]; } UICollectionViewLayout *layout = _collectionView.collectionViewLayout; UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init]; [context invalidateItemsAtIndexPaths:indexPaths]; [_collectionView performBatchUpdates:^{ [layout invalidateLayoutWithContext:context]; } completion:completion]; } #pragma mark - IGListBatchContext - (void)reloadInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { IGAssertMainThread(); IGParameterAssert(indexes != nil); IGParameterAssert(sectionController != nil); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Tried to reload the adapter from %@ without a collection view at indexes %@.", sectionController, indexes); if (indexes.count == 0) { return; } /** UICollectionView is not designed to support -reloadSections: or -reloadItemsAtIndexPaths: during batch updates. Internally it appears to convert these operations to a delete+insert. However the transformation is too simple in that it doesn't account for the item's section being moved (naturally or explicitly) and can queue animation collisions. If you have an object at section 2 with 4 items and attempt to reload item at index 1, you would create an NSIndexPath at section: 2, item: 1. Within -performBatchUpdates:, UICollectionView converts this to a delete and insert at the same NSIndexPath. If a section were inserted at position 2, the original section 2 has naturally shifted to section 3. However, the insert NSIndexPath is section: 2, item: 1. Now the UICollectionView has a section animation at section 2, as well as an item insert animation at section: 2, item: 1, and it will throw an exception. IGListAdapter tracks the before/after mapping of section controllers to make precise NSIndexPath conversions. */ [indexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { NSIndexPath *fromIndexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:YES]; NSIndexPath *toIndexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; // index paths could be nil if a section controller is prematurely reloading or a reload was batched with // the section controller being deleted if (fromIndexPath != nil && toIndexPath != nil) { [self.updater reloadItemInCollectionView:collectionView fromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } }]; } - (void)insertInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { IGAssertMainThread(); IGParameterAssert(indexes != nil); IGParameterAssert(sectionController != nil); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Inserting items from %@ without a collection view at indexes %@.", sectionController, indexes); if (indexes.count == 0) { return; } NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:NO]; [self.updater insertItemsIntoCollectionView:collectionView indexPaths:indexPaths]; if (![self isInDataUpdateBlock]) { [self _updateBackgroundView]; } } - (void)deleteInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { IGAssertMainThread(); IGParameterAssert(indexes != nil); IGParameterAssert(sectionController != nil); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Deleting items from %@ without a collection view at indexes %@.", sectionController, indexes); if (indexes.count == 0) { return; } NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:YES]; [self.updater deleteItemsFromCollectionView:collectionView indexPaths:indexPaths]; if (![self isInDataUpdateBlock]) { [self _updateBackgroundView]; } } - (void)invalidateLayoutInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { IGAssertMainThread(); IGParameterAssert(indexes != nil); IGParameterAssert(sectionController != nil); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Invalidating items from %@ without a collection view at indexes %@.", sectionController, indexes); if (indexes.count == 0) { return; } NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:NO]; UICollectionViewLayout *layout = collectionView.collectionViewLayout; UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init]; [context invalidateItemsAtIndexPaths:indexPaths]; [layout invalidateLayoutWithContext:context]; } - (void)moveInSectionController:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { IGAssertMainThread(); IGParameterAssert(sectionController != nil); IGParameterAssert(fromIndex >= 0); IGParameterAssert(toIndex >= 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Moving items from %@ without a collection view from index %li to index %li.", sectionController, (long)fromIndex, (long)toIndex); NSIndexPath *fromIndexPath = [self indexPathForSectionController:sectionController index:fromIndex usePreviousIfInUpdateBlock:YES]; NSIndexPath *toIndexPath = [self indexPathForSectionController:sectionController index:toIndex usePreviousIfInUpdateBlock:NO]; if (fromIndexPath == nil || toIndexPath == nil) { return; } [self.updater moveItemInCollectionView:collectionView fromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } - (void)reloadSectionController:(IGListSectionController *)sectionController { IGAssertMainThread(); IGParameterAssert(sectionController != nil); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Reloading items from %@ without a collection view.", sectionController); IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:YES]; const NSInteger section = [map sectionForSectionController:sectionController]; if (section == NSNotFound) { return; } NSIndexSet *sections = [NSIndexSet indexSetWithIndex:section]; [self.updater reloadCollectionView:collectionView sections:sections]; if (![self isInDataUpdateBlock]) { [self _updateBackgroundView]; } } - (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) { IGAssertMainThread(); IGParameterAssert(sectionController != nil); IGParameterAssert(fromIndex >= 0); IGParameterAssert(toIndex >= 0); UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Moving section %@ without a collection view from index %li to index %li.", sectionController, (long)fromIndex, (long)toIndex); IGAssert(self.moveDelegate != nil, @"Moving section %@ without a moveDelegate set", sectionController); if (fromIndex != toIndex) { id dataSource = self.dataSource; NSArray *previousObjects = [self.sectionMap objects]; if (self.isLastInteractiveMoveToLastSectionIndex) { self.isLastInteractiveMoveToLastSectionIndex = NO; } else if (fromIndex < toIndex) { toIndex -= 1; } NSMutableArray *mutObjects = [previousObjects mutableCopy]; id object = [previousObjects objectAtIndex:fromIndex]; [mutObjects removeObjectAtIndex:fromIndex]; [mutObjects insertObject:object atIndex:toIndex]; NSArray *objects = [mutObjects copy]; // inform the data source to update its model [self.moveDelegate listAdapter:self moveObject:object from:previousObjects to:objects]; // update our model based on that provided by the data source NSArray> *updatedObjects = [dataSource objectsForListAdapter:self]; [self _updateObjects:updatedObjects dataSource:dataSource]; } // even if from and to index are equal, we need to perform the "move" // iOS interactively moves items, not sections, so we might have actually moved the item // to the end of the preceeding section or beginning of the following section [self.updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex]; } - (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) { IGAssertMainThread(); IGParameterAssert(sectionController != nil); IGParameterAssert(fromIndex >= 0); IGParameterAssert(toIndex >= 0); [sectionController moveObjectFromIndex:fromIndex toIndex:toIndex]; } - (void)revertInvalidInteractiveMoveFromIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath NS_AVAILABLE_IOS(9_0) { UICollectionView *collectionView = self.collectionView; IGAssert(collectionView != nil, @"Reverting move without a collection view from %@ to %@.", sourceIndexPath, destinationIndexPath); // revert by moving back in the opposite direction [collectionView moveItemAtIndexPath:destinationIndexPath toIndexPath:sourceIndexPath]; } - (NSIndexPath *_Nullable)indexPathForItemAtPoint:(CGPoint)point { return [self.collectionView indexPathForItemAtPoint:point]; } - (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view { return [self.collectionView convertPoint:point fromView:view]; } @end ================================================ FILE: Source/IGListKit/IGListAdapterDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListDiffable.h" #else #import #endif @class IGListAdapter; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** Implement this protocol to provide data to an `IGListAdapter`. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapterDataSource) @protocol IGListAdapterDataSource /** Asks the data source for the objects to display in the list. @param listAdapter The list adapter requesting this information. @return An array of objects for the list. */ - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter; /** Asks the data source for a section controller for the specified object in the list. @param listAdapter The list adapter requesting this information. @param object An object in the list. @return A new section controller instance that can be displayed in the list. @note New section controllers should be initialized here for objects when asked. You may pass any other data to the section controller at this time. Section controllers are initialized for all objects whenever the `IGListAdapter` is created, updated, or reloaded. Section controllers are reused when objects are moved or updated. Maintaining the `-[IGListDiffable diffIdentifier]` guarantees this. */ - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object; /** Asks the data source for a view to use as the collection view background when the list is empty. @param listAdapter The list adapter requesting this information. @return A view to use as the collection view background, or `nil` if you don't want a background view. @note This method is called every time the list adapter is updated. You are free to return new views every time, but for performance reasons you may want to retain the view and return it here. The infra is only responsible for adding the background view and maintaining its visibility. */ - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @class IGListAdapter; NS_ASSUME_NONNULL_BEGIN /** Conform to `IGListAdapterDelegate` to receive display events for objects in a list. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapterDelegate) @protocol IGListAdapterDelegate /** Notifies the delegate that a list object is about to be displayed. @param listAdapter The list adapter sending this information. @param object The object that will display. @param index The index of the object in the list. */ - (void)listAdapter:(IGListAdapter *)listAdapter willDisplayObject:(id)object atIndex:(NSInteger)index; /** Notifies the delegate that a list object is no longer being displayed. @param listAdapter The list adapter sending this information. @param object The object that ended display. @param index The index of the object in the list. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingObject:(id)object atIndex:(NSInteger)index; /** Notifies the delegate that a list object is about to be displayed. This method is distinct from willDisplayObject:atIndex because this method gets called whenever a cell will be displayed on screen as opposed to willDisplayObject:atIndex which only gets called for once per section. @param listAdapter The list adapter sending this information. @param object The object that will display. @param cell The cell which contains the object that will display. @param indexPath The index path of the object in the list. */ - (void)listAdapter:(IGListAdapter *)listAdapter willDisplayObject:(id)object cell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; /** Notifies the delegate that a list object is no longer being displayed. This method is distinct from didEndDisplayingObject:atIndex because this method gets called whenever a cell ends display on screen as opposed to didEndDisplayingObject:atIndex which only gets called once when the section fully ends display. @param listAdapter The list adapter sending this information. @param object The object that ended display. @param cell The cell which contains the object that ended display. @param indexPath The index path of the object in the list. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingObject:(id)object cell:(UICollectionViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterDelegateAnnouncer.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListAdapterDelegate.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN @interface IGListAdapterDelegateAnnouncer : NSObject /// Default announcer for all `IGListAdapter` + (instancetype)sharedInstance; /// Add a delegate that will receive callbacks for all `IGListAdapter`. /// This is a weak reference, so you don't need to remove it on dealloc. - (void)addListener:(id)listener; /// Remove delegate - (void)removeListener:(id)listener; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterDelegateAnnouncer.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListAdapterDelegateAnnouncerInternal.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif @implementation IGListAdapterDelegateAnnouncer { NSHashTable> *_delegates; } + (instancetype)sharedInstance { static IGListAdapterDelegateAnnouncer *shared = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shared = [self new]; }); return shared; } - (void)addListener:(id)listener { IGAssertMainThread(); if (!_delegates) { _delegates = [NSHashTable weakObjectsHashTable]; } [_delegates addObject:listener]; } - (void)removeListener:(id)listener { IGAssertMainThread(); [_delegates removeObject:listener]; } - (void)announceObjectDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { IGAssertMainThread(); for (id delegate in [_delegates allObjects]) { [delegate listAdapter:listAdapter willDisplayObject:object atIndex:index]; } } - (void)announceObjectEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index { IGAssertMainThread(); for (id delegate in [_delegates allObjects]) { [delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:index]; } } - (void)announceCellDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object cell:(UICollectionViewCell *)cell indexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); for (id delegate in [_delegates allObjects]) { [delegate listAdapter:listAdapter willDisplayObject:object cell:cell atIndexPath:indexPath]; } } - (void)announceCellEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object cell:(UICollectionViewCell *)cell indexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); for (id delegate in [_delegates allObjects]) { [delegate listAdapter:listAdapter didEndDisplayingObject:object cell:cell atIndexPath:indexPath]; } } @end ================================================ FILE: Source/IGListKit/IGListAdapterMoveDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; NS_ASSUME_NONNULL_BEGIN /** Conform to `IGListAdapterMoveDelegate` to receive interactive reordering requests. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapterMoveDelegate) @protocol IGListAdapterMoveDelegate /** Asks the delegate to move a section object as the result of interactive reordering. @param listAdapter The list adapter sending this information. @param object the object that was moved @param previousObjects The array of objects prior to the move. @param objects The array of objects after the move. */ - (void)listAdapter:(IGListAdapter *)listAdapter moveObject:(id)object from:(NSArray *)previousObjects to:(NSArray *)objects; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterPerformanceDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** `IGListAdapterPerformanceDelegate` can be used to measure cell dequeue, display, size, and scroll callbacks. */ NS_SWIFT_NAME(ListAdapterPerformanceDelegate) @protocol IGListAdapterPerformanceDelegate /** Will call `-[IGListAdapter collectionView:cellForItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. */ - (void)listAdapterWillCallDequeueCell:(IGListAdapter *)listAdapter; /** Did finish calling `-[IGListAdapter collectionView:cellForItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. @param cell A cell that was dequeued. @param sectionController The section controller providing the cell. @param index Item index of the cell. */ - (void)listAdapter:(IGListAdapter *)listAdapter didCallDequeueCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Will call `-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. */ - (void)listAdapterWillCallDisplayCell:(IGListAdapter *)listAdapter; /** Did finish calling `-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. @param cell A cell that will be displayed. @param sectionController The section controller for that cell. @param index Item index of the cell. @note Keep in mind this also includes calling the `IGListAdapter`'s collectionViewDelegate and workingRangeHandler. */ - (void)listAdapter:(IGListAdapter *)listAdapter didCallDisplayCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Will call `-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. */ - (void)listAdapterWillCallEndDisplayCell:(IGListAdapter *)listAdapter; /** Did finish calling `-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. @param cell A cell that was displayed. @param sectionController The section controller for that cell. @param index Item index of the cell. @note Keep in mind this also includes calling the `IGListAdapter`'s collectionViewDelegate and workingRangeHandler. */ - (void)listAdapter:(IGListAdapter *)listAdapter didCallEndDisplayCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Will call `-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. */ - (void)listAdapterWillCallSize:(IGListAdapter *)listAdapter; /** Did finish calling `-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]`. @param listAdapter The list adapter sending this information. @param sectionController The section controller providing the size. @param index Item index used to calculate the size. */ - (void)listAdapter:(IGListAdapter *)listAdapter didCallSizeOnSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Will call `-[IGListAdapter scrollViewDidScroll:]`. @param listAdapter The list adapter sending this information. */ - (void)listAdapterWillCallScroll:(IGListAdapter *)listAdapter; /** Did finish calling `-[IGListAdapter scrollViewDidScroll:]`. @param listAdapter The list adapter sending this information. @param scrollView The scroll view backing the UICollectionView. @note Keep in mind this also includes calling the `IGListAdapter`'s scrollViewDelegate and all visible `IGListSectioControllers`. */ - (void)listAdapter:(IGListAdapter *)listAdapter didCallScroll:(UIScrollView *)scrollView; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterUpdateListener.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; NS_ASSUME_NONNULL_BEGIN /** The type of update that was performed by an `IGListAdapter`. */ NS_SWIFT_NAME(ListAdapterUpdateType) typedef NS_ENUM(NSInteger, IGListAdapterUpdateType) { /** `-[IGListAdapter performUpdatesAnimated:completion:]` was executed. */ IGListAdapterUpdateTypePerformUpdates, /** `-[IGListAdapter reloadDataWithCompletion:]` was executed. */ IGListAdapterUpdateTypeReloadData, /** `-[IGListCollectionContext performBatchAnimated:updates:completion:]` was executed by an `IGListSectionController`. */ IGListAdapterUpdateTypeItemUpdates, }; /** Conform to this protocol to receive events about `IGListAdapter` updates. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapterUpdateListener) @protocol IGListAdapterUpdateListener /** Notifies a listener that the listAdapter was updated. @param listAdapter The `IGListAdapter` that updated. @param update The type of update executed. @param animated A flag indicating if the update was animated. Always `NO` for `IGListAdapterUpdateTypeReloadData`. @note This event is sent before the completion block in `-[IGListAdapter performUpdatesAnimated:completion:]` and `-[IGListAdapter reloadDataWithCompletion:]` is executed. This event is also delivered when an `IGListSectionController` updates via `-[IGListCollectionContext performBatchAnimated:updates:completion:]`. */ - (void)listAdapter:(IGListAdapter *)listAdapter didFinishUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterUpdater.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #import "IGListExperiments.h" #else #import #import #endif #if !__has_include() #import "IGListUpdatingDelegate.h" #import "IGListAdapterUpdaterDelegate.h" #else #import #import #endif NS_ASSUME_NONNULL_BEGIN /** An `IGListAdapterUpdater` is a concrete type that conforms to `IGListUpdatingDelegate`. It is an out-of-box updater for `IGListAdapter` objects to use. @note This updater performs re-entrant, coalesced updating for a list. It also uses a least-minimal diff for calculating UI updates when `IGListAdapter` calls `-performUpdateWithCollectionView:fromObjects:toObjects:completion:`. */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListAdapterUpdater) @interface IGListAdapterUpdater : NSObject /** The delegate that receives events with data on the performance of a transition. */ @property (nonatomic, weak) id delegate; /** A flag indicating if a section move should be treated as a section "delete, then insert" operation. This can be useful if you're performing a lot of updates and moves are too distracting. Default is NO. */ @property (nonatomic, assign) BOOL sectionMovesAsDeletesInserts; /** ONLY used when there is N section, but each section only contains 1 item. We don't need to change move into delete+insert, and we dont need to call -reload at all. This unlocks many default UICollectionView animations: move/inline cell updates/deletes/inserts etc. Default is NO. @warning This should only work for Section that *ONLY* has single item setup. */ @property (nonatomic, assign) BOOL singleItemSectionUpdates; /** A flag indicating that section reloads should be treated as item reloads, instead of converting them to "delete, then insert" operations. This only applies if the number of items for the section is unchanged. Default is NO. @note If the number of items for the section is changed, we would fallback to the default behavior and convert it to "delete + insert", because the collectionView can crash otherwise. */ @property (nonatomic, assign) BOOL preferItemReloadsForSectionReloads; /** If there's more than 100 diff updates, fallback to using `reloadData` to avoid stalling the main thread. Default is YES. */ @property (nonatomic, assign) BOOL allowsReloadingOnTooManyUpdates; /** Allow the diffing to be performed on a background thread. Default is NO. */ @property (nonatomic, assign) BOOL allowsBackgroundDiffing; /** A bitmask of experiments to conduct on the updater. */ @property (nonatomic, assign) IGListExperiment experiments; /** This is an experimental feature to customize how diffing is performed. If there’s clear value, we’ll make this a real API and make breaking changes (i.e. replace `allowsBackgroundDiffing` with an enum). @warning - Keep in mind `allowsBackgroundDiffing` needs to be true for adaptive diffing to take effect. */ @property (nonatomic, assign) IGListAdaptiveDiffingExperimentConfig adaptiveDiffingExperimentConfig; /** Customize how coalescing works to speed up some updates */ @property (nonatomic, assign) IGListAdaptiveCoalescingExperimentConfig adaptiveCoalescingExperimentConfig; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListAdapterUpdater.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterUpdater.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListAdapterUpdaterHelpers.h" #import "IGListDefaultExperiments.h" #import "IGListIndexSetResultInternal.h" #import "IGListMoveIndexPathInternal.h" #import "IGListReloadIndexPath.h" #import "IGListTransitionData.h" #import "IGListUpdateTransactable.h" #import "IGListUpdateTransactionBuilder.h" #import "UICollectionView+IGListBatchUpdateData.h" #import "IGListUpdateCoalescer.h" @interface IGListAdapterUpdater () @property (nonatomic, strong) IGListUpdateTransactionBuilder *transactionBuilder; @property (nonatomic, strong) IGListUpdateCoalescer *coalescer; @property (nonatomic, strong, nullable) IGListUpdateTransactionBuilder *lastTransactionBuilder; @property (nonatomic, strong, nullable) id transaction; @end @implementation IGListAdapterUpdater - (instancetype)init { IGAssertMainThread(); if (self = [super init]) { _transactionBuilder = [IGListUpdateTransactionBuilder new]; _coalescer = [IGListUpdateCoalescer new]; _coalescer.delegate = self; _allowsReloadingOnTooManyUpdates = YES; _experiments = IGListDefaultExperiments(); } return self; } #pragma mark - Update - (BOOL)hasChanges { return [self.transactionBuilder hasChanges]; } - (void)_queueUpdateIfNeeded { IGAssertMainThread(); if (!self.transactionBuilder.hasChanges) { return; } // Will call `-performUpdateWithCoalescer` [self.coalescer queueUpdateForView:self.transactionBuilder.collectionView]; } - (void)performUpdateWithCoalescer:(IGListUpdateCoalescer *)coalescer { [self update]; } - (void)update { IGAssertMainThread(); if (![self.transactionBuilder hasChanges]) { return; } if (self.transaction && self.transaction.state != IGListBatchUpdateStateIdle) { return; } IGListUpdateTransactationConfig config = (IGListUpdateTransactationConfig) { .sectionMovesAsDeletesInserts = _sectionMovesAsDeletesInserts, .singleItemSectionUpdates = _singleItemSectionUpdates, .preferItemReloadsForSectionReloads = _preferItemReloadsForSectionReloads, .allowsReloadingOnTooManyUpdates = _allowsReloadingOnTooManyUpdates, .allowsBackgroundDiffing = _allowsBackgroundDiffing, .experiments = _experiments, .adaptiveDiffingExperimentConfig = _adaptiveDiffingExperimentConfig, }; id transaction = [self.transactionBuilder buildWithConfig:config delegate:_delegate updater:self]; self.transaction = transaction; self.lastTransactionBuilder = self.transactionBuilder; self.transactionBuilder = [IGListUpdateTransactionBuilder new]; if (!transaction) { // If we don't have enough information, we might not be able to create a transaction. self.lastTransactionBuilder = nil; return; } __weak __typeof__(self) weakSelf = self; __weak __typeof__(transaction) weakTransaction = transaction; [transaction addCompletionBlock:^(BOOL finished) { __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { return; } if (strongSelf.transaction == weakTransaction) { strongSelf.transaction = nil; strongSelf.lastTransactionBuilder = nil; // queue another update in case something changed during batch updates. this method will bail next runloop if // there are no changes [strongSelf _queueUpdateIfNeeded]; } }]; [transaction begin]; } - (BOOL)isInDataUpdateBlock { return self.transaction.state == IGListBatchUpdateStateExecutingBatchUpdateBlock; } #pragma mark - IGListUpdatingDelegate static BOOL IGListIsEqual(const void *a, const void *b, NSUInteger (*size)(const void *item)) { const id left = (__bridge id)a; const id right = (__bridge id)b; return [left class] == [right class] && [[left diffIdentifier] isEqual:[right diffIdentifier]]; } // since the diffing algo used in this updater keys items based on their -diffIdentifier, we must use a map table that // precisely mimics this behavior static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(const void *item)) { return [[(__bridge id)item diffIdentifier] hash]; } - (NSPointerFunctions *)objectLookupPointerFunctions { NSPointerFunctions *functions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; functions.hashFunction = IGListIdentifierHash; functions.isEqualFunction = IGListIsEqual; return functions; } - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(nullable IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); IGParameterAssert(sectionDataBlock != nil); IGParameterAssert(applySectionDataBlock != nil); [self.transactionBuilder addSectionBatchUpdateAnimated:animated collectionViewBlock:collectionViewBlock sectionDataBlock:sectionDataBlock applySectionDataBlock:applySectionDataBlock completion:completion]; [self _queueUpdateIfNeeded]; } - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated itemUpdates:(void (^)(void))itemUpdates completion:(void (^)(BOOL))completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); IGParameterAssert(itemUpdates != nil); // if already inside the execution of the update block, immediately unload the itemUpdates block. // the completion blocks are executed later in the lifecycle, so that still needs to be added to the batch if ([self isInDataUpdateBlock]) { if (completion != nil) { [self.transaction addCompletionBlock:completion]; } itemUpdates(); } else { [self.transactionBuilder addItemBatchUpdateAnimated:animated collectionViewBlock:collectionViewBlock itemUpdates:itemUpdates completion:completion]; [self _queueUpdateIfNeeded]; } } - (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(nullable IGListUpdatingCompletion)completion { IGAssertMainThread(); IGParameterAssert(collectionViewBlock != nil); IGParameterAssert(reloadUpdateBlock != nil); [self.transactionBuilder addReloadDataWithCollectionViewBlock:collectionViewBlock reloadBlock:reloadUpdateBlock completion:completion]; [self _queueUpdateIfNeeded]; } - (void)performDataSourceChange:(IGListDataSourceChangeBlock)block { // Unlike the other "performs", we need the dataSource change to be synchronous. // Which means we need to cancel the current transaction, flatten the changes from // both the current transtion and builder, and execute that new transaction. if (!self.transaction && ![self.transactionBuilder hasChanges] && !IGListExperimentEnabled(self.experiments, IGListExperimentRemoveDataSourceChangeEarlyExit)) { // If nothing is going on, lets take a shortcut. block(); return; } IGListUpdateTransactionBuilder *builder = [IGListUpdateTransactionBuilder new]; [builder addDataSourceChange:block]; // Lets try to cancel any current transactions. if ([self.transaction cancel] && self.lastTransactionBuilder) { // We still need to apply the item-updates and completion-blocks, so lets merge the builders. [builder addChangesFromBuilder:(IGListUpdateTransactionBuilder *)self.lastTransactionBuilder]; } // Lets merge pending changes [builder addChangesFromBuilder:self.transactionBuilder]; // Clear the current state self.transaction = nil; self.lastTransactionBuilder = nil; self.transactionBuilder = builder; // Update synchronously [self update]; } - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); if ([self isInDataUpdateBlock]) { [self.transaction insertItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView]; [collectionView insertItemsAtIndexPaths:indexPaths]; } } - (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(indexPaths != nil); if ([self isInDataUpdateBlock]) { [self.transaction deleteItemsAtIndexPaths:indexPaths]; } else { [self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView]; [collectionView deleteItemsAtIndexPaths:indexPaths]; } } - (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { if ([self isInDataUpdateBlock]) { [self.transaction moveItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } else { [self.delegate listAdapterUpdater:self willMoveFromIndexPath:fromIndexPath toIndexPath:toIndexPath collectionView:collectionView]; [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; } } - (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { if ([self isInDataUpdateBlock]) { [self.transaction reloadItemFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; } else { [self.delegate listAdapterUpdater:self willReloadIndexPaths:@[fromIndexPath] collectionView:collectionView]; [collectionView reloadItemsAtIndexPaths:@[fromIndexPath]]; } } - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { IGAssertMainThread(); IGParameterAssert(collectionView != nil); IGParameterAssert(sections != nil); if ([self isInDataUpdateBlock]) { [self.transaction reloadSections:sections]; } else { [self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView]; [collectionView reloadSections:sections]; } } - (void)moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { IGAssertMainThread(); IGParameterAssert(collectionView != nil); // iOS expects interactive reordering to be movement of items not sections // after moving a single-item section controller, // you end up with two items in the section for the drop location, // and zero items in the section originating at the drag location // so, we have to reload data rather than doing a section move [collectionView reloadData]; // It seems that reloadData called during UICollectionView's moveItemAtIndexPath // delegate call does not reload all cells as intended // So, we further reload all visible sections to make sure none of our cells // are left with data that's out of sync with our dataSource id delegate = self.delegate; NSArray *visibleIndexPaths = [collectionView indexPathsForVisibleItems]; NSIndexSet *visibleSections = IGListSectionIndexFromIndexPaths(visibleIndexPaths); [delegate listAdapterUpdater:self willReloadSections:visibleSections collectionView:collectionView]; // prevent double-animation from reloadData + reloadSections [CATransaction begin]; [CATransaction setDisableActions:YES]; [collectionView performBatchUpdates:^{ [collectionView reloadSections:visibleSections]; } completion:^(BOOL finished) { [CATransaction commit]; }]; } - (void)willCrashWithCollectionView:(UICollectionView *)collectionView sectionControllerClass:(Class)sectionControllerClass { [self.delegate listAdapterUpdater:self willCrashWithCollectionView:collectionView sectionControllerClass:sectionControllerClass]; } #pragma mark - Properties - (IGListAdaptiveCoalescingExperimentConfig)adaptiveCoalescingExperimentConfig { return _coalescer.adaptiveCoalescingExperimentConfig; } - (void)setAdaptiveCoalescingExperimentConfig:(IGListAdaptiveCoalescingExperimentConfig)adaptiveCoalescingExperimentConfig { _coalescer.adaptiveCoalescingExperimentConfig = adaptiveCoalescingExperimentConfig; } @end ================================================ FILE: Source/IGListKit/IGListAdapterUpdaterDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListBatchUpdateData.h" #else #import #endif @class IGListAdapterUpdater; @class IGListIndexSetResult; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN /** A protocol that receives events about `IGListAdapterUpdater` operations. */ NS_SWIFT_NAME(ListAdapterUpdaterDelegate) @protocol IGListAdapterUpdaterDelegate /** Notifies the delegate that the updater is about to beging diffing. @param listAdapterUpdater The adapter updater owning the transition. @param fromObjects The items transitioned from in the batch updates, if any. @param toObjects The items transitioned to in the batch updates, if any. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willDiffFromObjects:(nullable NSArray > *)fromObjects toObjects:(nullable NSArray > *)toObjects; /** Notifies the delegate that the updater finished diffing. @param listAdapterUpdater The adapter updater owning the transition. @param listIndexSetResults The diffing result of indices to be inserted/removed/updated/moved/etc. @param onBackgroundThread Was the diffing performed on a background thread */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater didDiffWithResults:(nullable IGListIndexSetResult *)listIndexSetResults onBackgroundThread:(BOOL)onBackgroundThread; /** Notifies the delegate that the updater will call `-[UICollectionView performBatchUpdates:completion:]`. @param listAdapterUpdater The adapter updater owning the transition. @param collectionView The collection view that will perform the batch updates. @param fromObjects The items transitioned from in the batch updates, if any. @param toObjects The items transitioned to in the batch updates, if any. @param listIndexSetResults The diffing result of indices to be inserted/removed/updated/moved/etc. @param animated Is the cell transtion animated */ - (void) listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willPerformBatchUpdatesWithCollectionView:(UICollectionView *)collectionView fromObjects:(nullable NSArray > *)fromObjects toObjects:(nullable NSArray > *)toObjects listIndexSetResult:(nullable IGListIndexSetResult *)listIndexSetResults animated:(BOOL)animated; /** Notifies the delegate that the updater successfully finished `-[UICollectionView performBatchUpdates:completion:]`. @param listAdapterUpdater The adapter updater owning the transition. @param updates The batch updates that were applied to the collection view. @param collectionView The collection view that performed the batch updates. @note This event is called in the completion block of the batch update. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater didPerformBatchUpdates:(IGListBatchUpdateData *)updates collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView insertItemsAtIndexPaths:]`. @param listAdapterUpdater The adapter updater owning the transition. @param indexPaths An array of index paths that will be inserted. @param collectionView The collection view that will perform the insert. @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willInsertIndexPaths:(NSArray *)indexPaths collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView deleteItemsAtIndexPaths:]`. @param listAdapterUpdater The adapter updater owning the transition. @param indexPaths An array of index paths that will be deleted. @param collectionView The collection view that will perform the delete. @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willDeleteIndexPaths:(NSArray *)indexPaths collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView moveItemAtIndexPath:toIndexPath:]` @param listAdapterUpdater The adapter updater owning the transition. @param fromIndexPath The index path of the item that will be moved. @param toIndexPath The index path to move the item to. @param collectionView The collection view that will perform the move. @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willMoveFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView reloadItemsAtIndexPaths:]`. @param listAdapterUpdater The adapter updater owning the transition. @param indexPaths An array of index paths that will be reloaded. @param collectionView The collection view that will perform the reload. @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willReloadIndexPaths:(NSArray *)indexPaths collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView reloadSections:]`. @param listAdapterUpdater The adapter updater owning the transition. @param sections The sections that will be reloaded @param collectionView The collection view that will perform the reload. @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willReloadSections:(NSIndexSet *)sections collectionView:(UICollectionView *)collectionView; /** Notifies the delegate that the updater will call `-[UICollectionView reloadData]`. @param listAdapterUpdater The adapter updater owning the transition. @param collectionView The collection view that will be reloaded. @param isFallbackReload The reload was a fallback because we could not performBatchUpdate */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willReloadDataWithCollectionView:(UICollectionView *)collectionView isFallbackReload:(BOOL)isFallbackReload; /** Notifies the delegate that the updater successfully called `-[UICollectionView reloadData]`. @param listAdapterUpdater The adapter updater owning the transition. @param collectionView The collection view that reloaded. @param isFallbackReload The reload was a fallback because we could not performBatchUpdate */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater didReloadDataWithCollectionView:(UICollectionView *)collectionView isFallbackReload:(BOOL)isFallbackReload; /** Notifies the delegate that the collection view threw an exception in `-[UICollectionView performBatchUpdates:completion:]`. @param listAdapterUpdater The adapter updater owning the transition. @param collectionView The collection view being updated. @param exception The exception thrown by the collection view. @param fromObjects The items transitioned from in the diff, if any. @param toObjects The items transitioned to in the diff, if any. @param diffResult The diff result that were computed from `fromObjects` and `toObjects`. @param updates The batch updates that were applied to the collection view. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater collectionView:(UICollectionView *)collectionView willCrashWithException:(NSException *)exception fromObjects:(nullable NSArray *)fromObjects toObjects:(nullable NSArray *)toObjects diffResult:(IGListIndexSetResult *)diffResult updates:(IGListBatchUpdateData *)updates; /** Notifies the delegate that the updater detected an imminent crash, such as when a section controller returns a nil cell. This provides an opportunity to log diagnostic information before the crash occurs. @param listAdapterUpdater The adapter updater that detected the issue. @param collectionView The collection view involved in the crash. @param sectionControllerClass The class of the section controller that caused the issue, if available. */ - (void) listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willCrashWithCollectionView:(UICollectionView *)collectionView sectionControllerClass:(nullable Class)sectionControllerClass; /** Notifies the delegate that the updater finished without performing any batch updates or reloads @param listAdapterUpdater The adapter updater owning the transition. @param collectionView The collection view that reloaded. */ - (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater didFinishWithoutUpdatesWithCollectionView:(nullable UICollectionView *)collectionView; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBatchContext.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** Objects conforming to the IGListBatchContext protocol provide a way for section controllers to mutate their cells or reload everything within the section. */ NS_SWIFT_NAME(ListBatchContext) @protocol IGListBatchContext /** Reloads cells in the section controller. @param sectionController The section controller who's cells need reloading. @param indexes The indexes of items that need reloading. */ - (void)reloadInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes; /** Inserts cells in the list. @param sectionController The section controller who's cells need inserting. @param indexes The indexes of items that need inserting. */ - (void)insertInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes; /** Deletes cells in the list. @param sectionController The section controller who's cells need deleted. @param indexes The indexes of items that need deleting. */ - (void)deleteInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes; /** Invalidates layouts of cells at specific in the section controller. @param sectionController The section controller who's cells need invalidating. @param indexes The indexes of items that need invalidating. */ - (void)invalidateLayoutInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes; /** Moves a cell from one index to another within the section controller. @param sectionController The section controller who's cell needs moved. @param fromIndex The index the cell is currently in. @param toIndex The index the cell should move to. */ - (void)moveInSectionController:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex; /** Reloads the entire section controller. @param sectionController The section controller who's cells need reloading. */ - (void)reloadSectionController:(IGListSectionController *)sectionController; /** Moves a section controller from one index to another during interactive reordering. @param sectionController The section controller to move. @param fromIndex The index where the section currently resides. @param toIndex The index the section should move to. */ - (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0); /** Moves an object within a section controller from one index to another during interactive reordering. @param sectionController The section controller containing the object to move. @param fromIndex The index where the object currently resides. @param toIndex The index the object should move to. */ - (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0); /** Reverts an move from one indexPath to another during interactive reordering. @param sourceIndexPath The indexPath the item was originally in. @param destinationIndexPath The indexPath the item was moving to. */ - (void)revertInvalidInteractiveMoveFromIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath NS_AVAILABLE_IOS(9_0); @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindable.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** A protocol for cells that configure themselves given a view model. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListBindable) @protocol IGListBindable /** Tells the cell to configure itself with the given view model. @param viewModel The view model for the cell. @note The view model can change many times throughout the lifetime of a cell as the model values change and the cell is reused. Implementations should use only this method to do their configuration. */ - (void)bindViewModel:(id)viewModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindingSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListBindingSectionControllerDataSource.h" #import "IGListBindingSectionControllerSelectionDelegate.h" #import "IGListSectionController.h" #else #import #import #import #endif @protocol IGListDiffable; @class IGListBindingSectionController; NS_ASSUME_NONNULL_BEGIN /** This section controller uses a data source to transform its "top level" object into an array of diffable view models. It then automatically binds each view model to cells via the `IGListBindable` protocol. Models used with `IGListBindingSectionController` should take special care to always return `YES` for identical objects. That is, any objects with matching `-diffIdentifier`s should always be equal, that way the section controller can create new view models via the data source, create a diff, and update the specific cells that have changed. In Objective-C, your `-isEqualToDiffableObject:` can simply be: ``` - (BOOL)isEqualToDiffableObject:(id)object { return YES; } ``` In Swift: ``` func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { return true } ``` Only when `-diffIdentifier`s match is object equality compared, so you can assume the class is the same, and the instance has already been checked. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListBindingSectionController) @interface IGListBindingSectionController<__covariant ObjectType : id> : IGListSectionController /** A data source that transforms a top-level object into view models, and returns cells and sizes for given view models. */ @property (nonatomic, weak, nullable) id dataSource; /** A delegate that receives selection events from cells in an `IGListBindingSectionController` instance. */ @property (nonatomic, weak, nullable) id selectionDelegate; /** The object currently assigned to the section controller, if any. */ @property (nonatomic, strong, readonly, nullable) ObjectType object; /** The array of view models created from the data source. Values are changed when the top-level object changes or by calling `-updateAnimated:completion:` manually. */ @property (nonatomic, copy, readonly) NSArray> *viewModels; /** Tells the section controller to query for new view models, diff the changes, and update its cells. @param animated A flag indicating if the transition should be animated or not. @param completion An optional completion block executed after updates finish. Parameter is YES if updates were applied. */ - (void)updateAnimated:(BOOL)animated completion:(nullable void (^)(BOOL updated))completion; /** Notifies the section that a list object should move within a section as the result of interactive reordering. @param sourceIndex The starting index of the object. @param destinationIndex The ending index of the object. @note this method must be implemented if interactive reordering is enabled. To ensure updating the internal viewModels array, **calling super is required**, preferably before your own implementation. */ - (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex NS_REQUIRES_SUPER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindingSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBindingSectionController.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListBindable.h" #import "IGListArrayUtilsInternal.h" typedef NS_ENUM(NSInteger, IGListDiffingSectionState) { IGListDiffingSectionStateIdle = 0, IGListDiffingSectionStateUpdateQueued, IGListDiffingSectionStateUpdateApplied }; @interface IGListBindingSectionController() @property (nonatomic, copy, readwrite) NSArray> *viewModels; @property (nonatomic, strong) id object; @property (nonatomic, assign) IGListDiffingSectionState state; @end @implementation IGListBindingSectionController #pragma mark - Public API - (void)updateAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { IGAssertMainThread(); if (self.state != IGListDiffingSectionStateIdle) { if (completion != nil) { completion(NO); } return; } self.state = IGListDiffingSectionStateUpdateQueued; __block IGListIndexSetResult *result = nil; __block NSArray> *oldViewModels = nil; id collectionContext = self.collectionContext; [self.collectionContext performBatchAnimated:animated updates:^(id batchContext) { if (self.state != IGListDiffingSectionStateUpdateQueued) { return; } oldViewModels = self.viewModels; id object = self.object; IGAssert(object != nil, @"Expected IGListBindingSectionController object to be non-nil before updating."); NSArray *newViewModels = [self.dataSource sectionController:self viewModelsForObject:object]; self.viewModels = objectsWithDuplicateIdentifiersRemoved(newViewModels); result = IGListDiff(oldViewModels, self.viewModels, IGListDiffEquality); [result.updates enumerateIndexesUsingBlock:^(NSUInteger oldUpdatedIndex, BOOL *stop) { id identifier = [oldViewModels[oldUpdatedIndex] diffIdentifier]; const NSInteger indexAfterUpdate = [result newIndexForIdentifier:identifier]; if (indexAfterUpdate != NSNotFound) { UICollectionViewCell *cell = [collectionContext cellForItemAtIndex:oldUpdatedIndex sectionController:self]; [cell bindViewModel:self.viewModels[indexAfterUpdate]]; } }]; if (IGListExperimentEnabled(self.collectionContext.experiments, IGListExperimentInvalidateLayoutForUpdates)) { [batchContext invalidateLayoutInSectionController:self atIndexes:result.updates]; } [batchContext deleteInSectionController:self atIndexes:result.deletes]; [batchContext insertInSectionController:self atIndexes:result.inserts]; for (IGListMoveIndex *move in result.moves) { [batchContext moveInSectionController:self fromIndex:move.from toIndex:move.to]; } self.state = IGListDiffingSectionStateUpdateApplied; } completion:^(BOOL __unused finished) { self.state = IGListDiffingSectionStateIdle; if (completion != nil) { completion(YES); } }]; } #pragma mark - IGListSectionController Overrides - (NSInteger)numberOfItems { return self.viewModels.count; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { if (index >= 0 && index < (NSInteger)self.viewModels.count) { return [self.dataSource sectionController:self sizeForViewModel:self.viewModels[index] atIndex:index]; } return CGSizeZero; } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { id viewModel = self.viewModels[index]; UICollectionViewCell *cell = [self.dataSource sectionController:self cellForViewModel:viewModel atIndex:index]; [cell bindViewModel:viewModel]; return cell; } - (void)didUpdateToObject:(id)object { id oldObject = self.object; self.object = object; if (oldObject == nil) { NSArray *viewModels = [self.dataSource sectionController:self viewModelsForObject:object]; self.viewModels = objectsWithDuplicateIdentifiersRemoved(viewModels); } else { #if defined(IGLK_LOGGING_ENABLED) && IGLK_LOGGING_ENABLED if (![oldObject isEqualToDiffableObject:object]) { IGLKLog(@"Warning: Unequal objects %@ and %@ will cause IGListBindingSectionController to reload the entire section", oldObject, object); } #endif [self updateAnimated:YES completion:nil]; } } - (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex { NSMutableArray *viewModels = [self.viewModels mutableCopy]; id modelAtSource = [viewModels objectAtIndex:sourceIndex]; [viewModels removeObjectAtIndex:sourceIndex]; [viewModels insertObject:modelAtSource atIndex:destinationIndex]; self.viewModels = viewModels; } - (void)didSelectItemAtIndex:(NSInteger)index { [self.selectionDelegate sectionController:self didSelectItemAtIndex:index viewModel:self.viewModels[index]]; } - (void)didDeselectItemAtIndex:(NSInteger)index { [self.selectionDelegate sectionController:self didDeselectItemAtIndex:index viewModel:self.viewModels[index]]; } - (void)didHighlightItemAtIndex:(NSInteger)index { [self.selectionDelegate sectionController:self didHighlightItemAtIndex:index viewModel:self.viewModels[index]]; } - (void)didUnhighlightItemAtIndex:(NSInteger)index { [self.selectionDelegate sectionController:self didUnhighlightItemAtIndex:index viewModel:self.viewModels[index]]; } #if !TARGET_OS_TV - (UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point { if (![self.selectionDelegate respondsToSelector:@selector(sectionController:contextMenuConfigurationForItemAtIndex:point:viewModel:)]) { return nil; } return [self.selectionDelegate sectionController:self contextMenuConfigurationForItemAtIndex:index point:point viewModel:self.viewModels[index]]; } #endif @end ================================================ FILE: Source/IGListKit/IGListBindingSectionControllerDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListBindingSectionController; @protocol IGListBindable; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN /** A protocol that returns data to power cells in an `IGListBindingSectionController`. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListBindingSectionControllerDataSource) @protocol IGListBindingSectionControllerDataSource /** Create an array of view models given a top-level object. @param sectionController The section controller requesting view models. @param object The top-level object that powers the section controller. @return A new array of view models. */ - (NSArray> *)sectionController:(IGListBindingSectionController *)sectionController viewModelsForObject:(id)object; /** Return a dequeued cell for a given view model. @param sectionController The section controller requesting a cell. @param viewModel The view model for the cell. @param index The index of the view model. @return A dequeued cell. @note The section controller will call `-bindViewModel:` with the provided view model after the cell is dequeued. You should handle cell configuration using this method. However, you can do additional configuration at this stage as well. */ - (UICollectionViewCell *)sectionController:(IGListBindingSectionController *)sectionController cellForViewModel:(id)viewModel atIndex:(NSInteger)index; /** Return a cell size for a given view model. @param sectionController The section controller requesting a size. @param viewModel The view model for the cell. @param index The index of the view model. @return A size for the view model. */ - (CGSize)sectionController:(IGListBindingSectionController *)sectionController sizeForViewModel:(id)viewModel atIndex:(NSInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListBindingSectionController; @class UIContextMenuConfiguration; NS_ASSUME_NONNULL_BEGIN /** A protocol that handles cell selection events in an `IGListBindingSectionController`. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListBindingSectionControllerSelectionDelegate) @protocol IGListBindingSectionControllerSelectionDelegate /** Tells the delegate that a cell at a given index was selected. @param sectionController The section controller the selection occurred in. @param index The index of the selected cell. @param viewModel The view model that was bound to the cell. */ - (void)sectionController:(IGListBindingSectionController *)sectionController didSelectItemAtIndex:(NSInteger)index viewModel:(id)viewModel; /** Tells the delegate that a cell at a given index was deselected. @param sectionController The section controller the deselection occurred in. @param index The index of the deselected cell. @param viewModel The view model that was bound to the cell. */ - (void)sectionController:(IGListBindingSectionController *)sectionController didDeselectItemAtIndex:(NSInteger)index viewModel:(id)viewModel; /** Tells the delegate that a cell at a given index was highlighted. @param sectionController The section controller the highlight occurred in. @param index The index of the highlighted cell. @param viewModel The view model that was bound to the cell. */ - (void)sectionController:(IGListBindingSectionController *)sectionController didHighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel; /** Tells the delegate that a cell at a given index was unhighlighted. @param sectionController The section controller the unhighlight occurred in. @param index The index of the unhighlighted cell. @param viewModel The view model that was bound to the cell. */ - (void)sectionController:(IGListBindingSectionController *)sectionController didUnhighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel; @optional /** Tells the delegate that a cell has requested a menu configuration. @param sectionController The section controller the request of a menu configuration occurred in. @param index The index of the cell that is being longed tap. @param point The point of the tap on the cell. @param viewModel The view model that was bound to the cell. @return An object that conforms to `UIContextMenuConfiguration`. */ - (UIContextMenuConfiguration * _Nullable)sectionController:(IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos); @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindingSingleSectionController.h ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListSectionController.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** Special section controller that only contains a single item, and it will apply the view model update during -didUpdateObject: call, usually happened inside -[UICollectionView performBatchUpdates:completion:]. This class is intended to be subclassed. */ NS_SWIFT_NAME(ListBindingSingleSectionController) @interface IGListBindingSingleSectionController<__covariant ViewModel : id, Cell : UICollectionViewCell *> : IGListSectionController #pragma mark - Subclass // Required to be implemented by subclass. - (Class)cellClass; // Required to be implemented by subclass. - (void)configureCell:(Cell)cell withViewModel:(ViewModel)viewModel; // Required to be implemented by subclass. - (CGSize)sizeForViewModel:(ViewModel)viewModel; // Subclasable. Defaults is no-op. - (void)didSelectItemWithCell:(Cell)cell; // Subclasable. Defaults is no-op. - (void)didDeselectItemWithCell:(Cell)cell; // Subclasable. Defaults is no-op. - (void)didHighlightItemWithCell:(Cell)cell; // Subclasable. Defaults is no-op.0 - (void)didUnhighlightItemWithCell:(Cell)cell; - (BOOL)isDisplayingCell; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListBindingSingleSectionController.m ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBindingSingleSectionController.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListSectionControllerInternal.h" @interface IGListBindingSingleSectionController () @end @implementation IGListBindingSingleSectionController { id _item; __weak UICollectionViewCell *_displayingCell; } - (void)didSelectItemWithCell:(UICollectionViewCell *)cell { // no-op } - (void)didDeselectItemWithCell:(UICollectionViewCell *)cell { // no-op } - (void)didHighlightItemWithCell:(UICollectionViewCell *)cell { // no-op } - (void)didUnhighlightItemWithCell:(UICollectionViewCell *)cell { // no-op } - (Class)cellClass { IGFailAssert(@"Implemented by subclass"); return nil; } - (void)configureCell:(UICollectionViewCell *)cell withViewModel:(id)viewModel { IGFailAssert(@"Implemented by subclass"); } - (CGSize)sizeForViewModel:(id)viewModel { IGFailAssert(@"Implemented by subclass"); return CGSizeZero; } #pragma mark - IGListSectionController Overrides - (NSInteger)numberOfItems { return 1; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); return [self sizeForViewModel:_item]; } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); UICollectionViewCell *cell = [self.collectionContext dequeueReusableCellOfClass:[self cellClass] forSectionController:self atIndex:index]; IGAssert(cell != nil, @"could not find a cell of class %@", NSStringFromClass([self cellClass])); [self configureCell:cell withViewModel:_item]; return cell; } - (void)didUpdateToObject:(id)object { if ([_item isEqualToDiffableObject:object]) { return; } _item = object; if (_displayingCell) { [self configureCell:_displayingCell withViewModel:_item]; } } - (void)didSelectItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; [self didSelectItemWithCell:cell]; } - (void)didDeselectItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; [self didDeselectItemWithCell:cell]; } - (void)didHighlightItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; [self didHighlightItemWithCell:cell]; } - (void)didUnhighlightItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; [self didUnhighlightItemWithCell:cell]; } - (void)willDisplayCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter { IGParameterAssert(index == 0); _displayingCell = cell; [super willDisplayCell:cell atIndex:index listAdapter:listAdapter]; } - (void)didEndDisplayingCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter { IGParameterAssert(index == 0); if (cell == _displayingCell) { _displayingCell = nil; } [super didEndDisplayingCell:cell atIndex:index listAdapter:listAdapter]; } - (BOOL)isDisplayingCell { return _displayingCell != nil; } @end ================================================ FILE: Source/IGListKit/IGListCollectionContext.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListExperiments.h" #else #import #endif #if !__has_include() #import "IGListBatchContext.h" #import "IGListCollectionScrollingTraits.h" #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @class IGListSectionController; /** The collection context provides limited access to the collection-related information that section controllers need for operations like sizing, dequeuing cells, inserting, deleting, reloading, etc. */ NS_SWIFT_NAME(ListCollectionContext) @protocol IGListCollectionContext /** The size of the collection view. You can use this for sizing cells. */ @property (nonatomic, readonly) CGSize containerSize; /** The content insets of the collection view. You can use this for sizing cells. */ @property (nonatomic, readonly) UIEdgeInsets containerInset; /** The adjusted content insets of the collection view. Equivalent to containerInset under iOS 11. */ @property (nonatomic, readonly) UIEdgeInsets adjustedContainerInset; /** The size of the collection view with content insets applied. */ @property (nonatomic, readonly) CGSize insetContainerSize; /** The content offset of the collection view. */ @property (nonatomic, readonly) CGPoint containerContentOffset; /** The trait collection of the collection view. */ @property (nonatomic, nullable, readonly) UITraitCollection *traitCollection; /** The current scrolling traits of the underlying collection view. */ @property (nonatomic, readonly) IGListCollectionScrollingTraits scrollingTraits; /** A bitmask of experiments to conduct on the section controller. */ @property (nonatomic, assign) IGListExperiment experiments; /** Returns size of the collection view relative to the section controller. @param sectionController The section controller requesting this information. @return The size of the collection view minus the given section controller's insets. */ - (CGSize)containerSizeForSectionController:(IGListSectionController *)sectionController; /** Returns the index of the specified cell in the collection relative to the section controller. @param cell An existing cell in the collection. @param sectionController The section controller requesting this information. @return The index of the cell or `NSNotFound` if it does not exist in the collection. */ - (NSInteger)indexForCell:(UICollectionViewCell *)cell sectionController:(IGListSectionController *)sectionController; /** Returns the cell in the collection at the specified index for the section controller. @param index The index of the desired cell. @param sectionController The section controller requesting this information. @return The collection view cell, or `nil` if not found. @warning This method may return `nil` if the cell is offscreen. */ - (nullable __kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController; /** Returns the supplementary view in the collection at the specified index for the section controller. @param elementKind The element kind of the supplementary view. @param index The index of the desired cell. @param sectionController The section controller requesting this information. @return The collection reusable view, or `nil` if not found. @warning This method may return `nil` if the cell is offscreen. */ - (nullable __kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController; /** Returns the fully visible cells for the given section controller. @param sectionController The section controller requesting this information. @return An array of fully visible cells, or an empty array if none are found. */ - (NSArray *)fullyVisibleCellsForSectionController:(IGListSectionController *)sectionController; /** Returns the visible cells for the given section controller. @param sectionController The section controller requesting this information. @return An array of visible cells, or an empty array if none are found. */ - (NSArray *)visibleCellsForSectionController:(IGListSectionController *)sectionController; /** Returns the visible paths for the given section controller. @param sectionController The section controller requesting this information. @return An array of visible index paths, or an empty array if none are found. */ - (NSArray *)visibleIndexPathsForSectionController:(IGListSectionController *) sectionController; /** Deselects a cell in the collection. @param index The index of the item to deselect. @param sectionController The section controller requesting this information. @param animated Pass `YES` to animate the change, `NO` otherwise. */ - (void)deselectItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController animated:(BOOL)animated; /** Selects a cell in the collection. @param index The index of the item to select. @param sectionController The section controller requesting this information. @param animated Pass `YES` to animate the change, `NO` otherwise. @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. */ - (void)selectItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController animated:(BOOL)animated scrollPosition:(UICollectionViewScrollPosition)scrollPosition; /** Dequeues a cell from the collection view reuse pool. @param cellClass The class of the cell you want to dequeue. @param reuseIdentifier A reuse identifier for the specified cell. This parameter may be `nil`. @param sectionController The section controller requesting this information. @param index The index of the cell. @return A cell dequeued from the reuse pool or a newly created one. @note This method uses a string representation of the cell class as the identifier. */ - (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass withReuseIdentifier:(nullable NSString *)reuseIdentifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Dequeues a cell from the collection view reuse pool. @param cellClass The class of the cell you want to dequeue. @param sectionController The section controller requesting this information. @param index The index of the cell. @return A cell dequeued from the reuse pool or a newly created one. @note This method uses a string representation of the cell class as the identifier. */ - (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Dequeues a cell from the collection view reuse pool. @param nibName The name of the nib file. @param bundle The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. @param sectionController The section controller requesting this information. @param index The index of the cell. @return A cell dequeued from the reuse pool or a newly created one. @note This method uses the nib name as the reuse identifier. */ - (__kindof UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName bundle:(nullable NSBundle *)bundle forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Dequeues a storyboard prototype cell from the collection view reuse pool. @param identifier The identifier of the cell prototype in storyboard. @param sectionController The section controller requesting this information. @param index The index of the cell. @return A cell dequeued from the reuse pool or a newly created one. */ - (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Dequeues a supplementary view from the collection view reuse pool. @param elementKind The kind of supplementary view. @param sectionController The section controller requesting this information. @param viewClass The class of the supplementary view. @param index The index of the supplementary view. @return A supplementary view dequeued from the reuse pool or a newly created one. @note This method uses a string representation of the view class and the kind as the identifier. */ - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind forSectionController:(IGListSectionController *)sectionController class:(Class)viewClass atIndex:(NSInteger)index; /** Dequeues a supplementary view from the collection view reuse pool. @param elementKind The kind of supplementary view. @param identifier The identifier of the supplementary view in storyboard. @param sectionController The section controller requesting this information. @param index The index of the supplementary view. @return A supplementary view dequeued from the reuse pool or a newly created one. */ - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind withIdentifier:(NSString *)identifier forSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Dequeues a supplementary view from the collection view reuse pool. @param elementKind The kind of supplementary view. @param sectionController The section controller requesting this information. @param nibName The name of the nib file. @param bundle The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. @param index The index of the supplementary view. @return A supplementary view dequeued from the reuse pool or a newly created one. @note This method uses the nib name as the reuse identifier. */ - (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind forSectionController:(IGListSectionController *)sectionController nibName:(NSString *)nibName bundle:(nullable NSBundle *)bundle atIndex:(NSInteger)index; /** Invalidate the backing `UICollectionViewLayout` for all items in the section controller. @param sectionController The section controller that needs invalidating. @param completion An optional completion block to execute when the updates are finished. @note This method can be wrapped in `UIView` animation APIs to control the duration or perform without animations. This will end up calling `-[UICollectionView performBatchUpdates:completion:]` internally, so invalidated changes may not be reflected in the cells immediately. */ - (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController completion:(nullable void (^)(BOOL finished))completion; /** Returns the layout attributes for the item at the specified index in the section controller. @param index The index of the item whose layout attributes are requested. @param sectionController The section controller requesting this information. @return The layout attributes for the item, or `nil` if the item is not found. */ - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndex:(NSInteger)index sectionController:(IGListSectionController *)sectionController; /** Batches and performs many cell-level updates in a single transaction. @param animated A flag indicating if the transition should be animated. @param updates A block with a context parameter to make mutations. @param completion An optional completion block to execute when the updates are finished. @note You should make state changes that impact the number of items in your section controller within the updates block alongside changes on the context object. For example, inside your section controllers, you may want to delete *and* insert into the data source that backs your section controller. For example: ``` [self.collectionContext performBatchItemUpdates:^ (id batchContext>){ // perform data source changes inside the update block [self.items addObject:newItem]; [self.items removeObjectAtIndex:0]; NSIndexSet *inserts = [NSIndexSet indexSetWithIndex:[self.items count] - 1]; [batchContext insertInSectionController:self atIndexes:inserts]; NSIndexSet *deletes = [NSIndexSet indexSetWithIndex:0]; [batchContext deleteInSectionController:self atIndexes:deletes]; } completion:nil]; ``` @warning You **must** perform data modifications **inside** the update block. Updates will not be performed synchronously, so you should make sure that your data source changes only when necessary. */ - (void)performBatchAnimated:(BOOL)animated updates:(void (^)(id batchContext))updates completion:(nullable void (^)(BOOL finished))completion; /** Scrolls to the specified section controller in the list. @param sectionController The section controller. @param index The index of the item in the section controller to which to scroll. @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. @param animated A flag indicating if the scrolling should be animated. */ - (void)scrollToSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index scrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated; /** Returns the index path of the item at the specified point in the collection view. @param point The point in the collection view's coordinate system. @return The index path of the item at the specified point, or `nil` if no item is found at that location. */ - (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point; /** Converts a point from the coordinate system of a given view to that of the collection view. @param point The point to convert. @param view The view from which the point originates. If `nil`, the point is assumed to be in the window's coordinate system. @return The converted point in the collection view's coordinate system. */ - (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListCollectionScrollingTraits.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import /** The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view. */ NS_SWIFT_NAME(ListCollectionScrollingTraits) typedef struct IGListCollectionScrollingTraits { /// returns YES if user has touched. may not yet have started dragging. bool isTracking; /// returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging bool isDragging; /// returns YES if user isn't dragging (touch up) but scroll view is still moving. bool isDecelerating; } IGListCollectionScrollingTraits; ================================================ FILE: Source/IGListKit/IGListCollectionView.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @protocol IGListCollectionViewLayoutCompatible; NS_ASSUME_NONNULL_BEGIN /** This `UICollectionView` subclass allows for partial layout invalidation using `IGListCollectionViewLayout`, or custom layout classes that conform to IGListCollectionViewLayoutCompatible. @note When updating a collection view (ex: calling `-insertSections`), `-invalidateLayoutWithContext` gets called on the layout object. However, the invalidation context doesn't provide details on which index paths are being modified, which typically forces a full layout re-calculation. `IGListCollectionView` gives `IGListCollectionViewLayout` the missing information to re-calculate only the modified layout attributes. */ NS_SWIFT_NAME(ListCollectionView) @interface IGListCollectionView : UICollectionView /** Create a new view with an `IGListcollectionViewLayout` class or subclass. @param frame The frame to initialize with. @param collectionViewLayout The layout to use with the collection view. You can use IGListCollectionViewLayout here, or a custom layout class that conforms to IGListCollectionViewLayoutCompatible. @note You can initialize a new view with a base layout by simply calling `-[IGListCollectionView initWithFrame:]`. */ - (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout NS_DESIGNATED_INITIALIZER; /** :nodoc: */ - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)collectionViewLayout NS_UNAVAILABLE; /** :nodoc: */ - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListCollectionView.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListCollectionView.h" #import "IGListCollectionViewLayout.h" #import "IGListCollectionViewLayoutCompatible.h" @implementation IGListCollectionView #pragma mark - Init - (instancetype)initWithFrame:(CGRect)frame { IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; return [self initWithFrame:frame listCollectionViewLayout:layout]; } - (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout { return [super initWithFrame:frame collectionViewLayout:collectionViewLayout]; } #pragma mark - IGListCollectionViewLayout - (UICollectionViewLayout *)_listLayout { if ([self.collectionViewLayout conformsToProtocol:@protocol(IGListCollectionViewLayoutCompatible)]) { return (UICollectionViewLayout *)self.collectionViewLayout; } return nil; } #pragma mark - Overrides reloads - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { [self _didModifyIndexPaths:indexPaths]; [super reloadItemsAtIndexPaths:indexPaths]; } - (void)reloadSections:(NSIndexSet *)sections { [self _didModifySections:sections]; [super reloadSections:sections]; } #pragma mark - Override deletes - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { [self _didModifyIndexPaths:indexPaths]; [super deleteItemsAtIndexPaths:indexPaths]; } - (void)deleteSections:(NSIndexSet *)sections { [self _didModifySections:sections]; [super deleteSections:sections]; } #pragma mark - Override inserts - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { [self _didModifyIndexPaths:indexPaths]; [super insertItemsAtIndexPaths:indexPaths]; } - (void)insertSections:(NSIndexSet *)sections { [self _didModifySections:sections]; [super insertSections:sections]; } #pragma mark - Override moves - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { [self _didModifyIndexPaths:@[indexPath, newIndexPath]]; [super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { [self _didModifySection:MIN(section, newSection)]; [super moveSection:section toSection:newSection]; } #pragma mark - Modify section - (void)_didModifySections:(NSIndexSet *)sections { if (sections.count == 0) { return; } [self _didModifySection:sections.firstIndex]; } - (void)_didModifySection:(NSUInteger)section { [self._listLayout didModifySection:section]; } #pragma mark - Modified index path - (void)_didModifyIndexPaths:(NSArray *)indexPaths { for (NSIndexPath *indexPath in indexPaths) { [self _didModifySection:indexPath.section]; } } @end ================================================ FILE: Source/IGListKit/IGListCollectionViewDelegateLayout.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import /** Conform to `IGListCollectionViewDelegateLayout` to provide customized layout information for a collection view. */ @protocol IGListCollectionViewDelegateLayout /** Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view. @param collectionView The collection view to perform the transition on. @param collectionViewLayout The layout to use with the collection view. @param attributes The starting layout information for an item being inserted into the collection view. @param indexPath The index path of the item being inserted. */ - (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath; /** Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view. @param collectionView The collection view to perform the transition on. @param collectionViewLayout The layout to use with the collection view. @param attributes The final layout information for an item that is about to be removed from the collection view. @param indexPath The index path of the item being deleted. */ - (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath; @end ================================================ FILE: Source/IGListKit/IGListCollectionViewLayout.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListCollectionViewLayoutCompatible.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, care must be taken not to stall the main thread in sizing delegate methods. This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that: - Your UICollectionView data source must also conform to UICollectionViewDelegateFlowLayout - Header support given via UICollectionElementKindSectionHeader All UICollectionViewDelegateFlowLayout methods are required and used by this layout: ``` - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section; - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section; - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; ``` In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position of an item extends beyond the width of the collection view. When that happens, the item is "newlined" to the next row. The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row. Ex. of a section (2,0) with a large width causing a newline. ``` |[ 0,0 ][ 1,0 ] | |[ 2,0 ]| ``` A section with a non-zero height header will always cause that section to newline. Headers are always stretched to the width of the collection view, pinched with the section insets. Ex. of a section (2,0) with a header inset on the left/right. ``` |[ 0,0 ][ 1,0 ] | | >======header=======< | | [ 2,0 ] | ``` Section insets apply to items in the section no matter if they begin on a new row or are on the same row as a previous section. Ex. of a section (2) with multiple items and a left inset. ``` |[ 0,0 ][ 1,0 ] >[ 2,0 ]| | >[ 2,1 ][ 2,2 ][ 2,3 ]| ``` Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same section. In a horizontally scrolling layout, sections and items are flowed vertically until they need to be "newlined" to the next column. Headers, if used, are stretched to the height of the collection view, minus the section insets. Please see the unit tests for more configuration examples and expected output. */ NS_SWIFT_NAME(ListCollectionViewLayout) @interface IGListCollectionViewLayout : UICollectionViewLayout /** Direction in which layout will be scrollable; items will be flowed in the perpendicular direction, "newlining" when they run out of space along that axis or when a non-zero header is found. */ @property (nonatomic, readonly) UICollectionViewScrollDirection scrollDirection; /** Set this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky header position as UI like the navigation bar is scrolled offscreen. In a vertically scrolling layout, changing this to the height of the navigation bar will give the effect of the headers sticking to the nav as it is collapsed. @note Changing the value on this method will invalidate the layout every time. */ @property (nonatomic, assign) CGFloat stickyHeaderYOffset; /** Set this to `YES` to show sticky header when a section had no item. Default is `NO`. */ @property (nonatomic, assign) BOOL showHeaderWhenEmpty; /** Set this to `YES` to keep layout cache when invalidateLayout is called. Default is `NO`. */ @property (nonatomic, assign) BOOL preserveLayoutCacheOnInvalidateLayout; /** Create and return a new collection view layout. @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. @param scrollDirection Direction along which the collection view will be scrollable (if content size exceeds the frame size) @param topContentInset The content inset (top or left, depending on scrolling direction) used to offset the sticky headers. Ignored if stickyHeaders is `NO`. @param stretchToEdge Specifies whether to stretch width (in vertically scrolling layout) or height (horizontally scrolling) of last item to right/bottom edge when distance from last item to right/bottom edge < epsilon(1) @return A new collection view layout. */ - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders scrollDirection:(UICollectionViewScrollDirection)scrollDirection topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER; /** Create and return a new vertically scrolling collection view layout. @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. @param topContentInset The top content inset used to offset the sticky headers. Ignored if stickyHeaders is `NO`. @param stretchToEdge Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1) @return A new collection view layout. */ - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; /** :nodoc: */ - (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListCollectionViewLayout.mm ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListCollectionViewLayout.h" #import "IGListCollectionViewLayoutInternal.h" #import #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListCollectionViewDelegateLayout.h" #import "IGListCollectionViewLayoutInvalidationContext.h" #import "UIScrollView+IGListKit.h" #import "IGListAdapter.h" static CGFloat UIEdgeInsetsLeadingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return insets.top; case UICollectionViewScrollDirectionHorizontal: return insets.left; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat UIEdgeInsetsTrailingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return insets.bottom; case UICollectionViewScrollDirectionHorizontal: return insets.right; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat CGPointGetCoordinateInDirection(CGPoint point, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return point.y; case UICollectionViewScrollDirectionHorizontal: return point.x; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat CGRectGetLengthInDirection(CGRect rect, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return rect.size.height; case UICollectionViewScrollDirectionHorizontal: return rect.size.width; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat CGRectGetMaxInDirection(CGRect rect, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return CGRectGetMaxY(rect); case UICollectionViewScrollDirectionHorizontal: return CGRectGetMaxX(rect); default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat CGRectGetMinInDirection(CGRect rect, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return CGRectGetMinY(rect); case UICollectionViewScrollDirectionHorizontal: return CGRectGetMinX(rect); default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static CGFloat CGSizeGetLengthInDirection(CGSize size, UICollectionViewScrollDirection direction) { switch (direction) { case UICollectionViewScrollDirectionVertical: return size.height; case UICollectionViewScrollDirectionHorizontal: return size.width; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, direction); } } static NSIndexPath *indexPathForSection(NSInteger section) { return [NSIndexPath indexPathForItem:0 inSection:section]; } static NSInteger IGListMergeMinimumInvalidatedSection(NSInteger section, NSInteger otherSection) { if (section == NSNotFound && otherSection == NSNotFound) { return NSNotFound; } if (section == NSNotFound) { return otherSection; } if (otherSection == NSNotFound) { return section; } return MIN(section, otherSection); } struct IGListSectionEntry { /** Represents the minimum-bounding box of every element in the section. This includes all item frames as well as the header bounds. It is made simply by unioning all item and header frames. Use this to find section intersections to build layout attributes given a rect. */ CGRect bounds; // The insets for the section. Used to find total content size of the section. UIEdgeInsets insets; // The RESTING frame of the header view (e.g. when the header is not sticking to the top of the scroll view). CGRect headerBounds; // The RESTING frame of the footer view CGRect footerBounds; // An array of frames for each cell in the section. std::vector itemBounds; // last item distance in scroll direction, used for partial invalidation CGFloat lastItemCoordInScrollDirection; // last item distance in fixed direction, used for partial invalidation CGFloat lastItemCoordInFixedDirection; // last next row distance in scroll direction, used for partial invalidation CGFloat lastNextRowCoordInScrollDirection; // Returns YES when the section has visible content (header and/or items). BOOL isValid() { return !CGSizeEqualToSize(bounds.size, CGSizeZero); } }; // Each section has a base zIndex of section * maxZIndexPerSection; // section header adds (maxZIndexPerSection - 1) to the base zIndex; // other cells adds (item) to the base zIndex. // This allows us to present tooltips that can grow from the cell to its top. static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attributes) { const NSInteger maxZIndexPerSection = 1000; const NSInteger baseZIndex = attributes.indexPath.section * maxZIndexPerSection; if (attributes.representedElementCategory == UICollectionElementCategoryCell) { attributes.zIndex = baseZIndex + attributes.indexPath.item; } else if (attributes.representedElementCategory == UICollectionElementCategorySupplementaryView) { attributes.zIndex = baseZIndex + maxZIndexPerSection - 1; } } @interface IGListCollectionViewLayout () @property (nonatomic, assign, readonly) BOOL stickyHeaders; @property (nonatomic, assign, readonly) CGFloat topContentInset; @property (nonatomic, assign, readonly) BOOL stretchToEdge; @end @implementation IGListCollectionViewLayout { std::vector _sectionData; NSMutableDictionary *_attributesCache; // invalidate starting at this section NSInteger _minimumInvalidatedSection; /** The workflow for getting sticky headers working: 1. Use a custom invalidation context to mark supplementary attributes invalid. 2. Return YES from -shouldInvalidateLayoutForBoundsChange: 3. In -invalidationContextForBoundsChange: mark supplementary attributes invalid on the custom context. 4. Purge supplementary caches in -invalidateLayoutWithContext: if context says they are invalid 5. Use cached attributes in -layoutAttributesForSupplementaryViewOfKind:atIndexPath: if they exist, else rebuild 6. Make sure -layoutAttributesForElementsInRect: always uses the attributes returned from -layoutAttributesForSupplementaryViewOfKind:atIndexPath:. */ NSMutableDictionary *> *_supplementaryAttributesCache; } - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge { return [self initWithStickyHeaders:stickyHeaders scrollDirection:UICollectionViewScrollDirectionVertical topContentInset:topContentInset stretchToEdge:stretchToEdge]; } - (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders scrollDirection:(UICollectionViewScrollDirection)scrollDirection topContentInset:(CGFloat)topContentInset stretchToEdge:(BOOL)stretchToEdge { if (self = [super init]) { _scrollDirection = scrollDirection; _stickyHeaders = stickyHeaders; _topContentInset = topContentInset; _stretchToEdge = stretchToEdge; _attributesCache = [NSMutableDictionary new]; _supplementaryAttributesCache = [NSMutableDictionary dictionaryWithDictionary:@{ UICollectionElementKindSectionHeader: [NSMutableDictionary new], UICollectionElementKindSectionFooter: [NSMutableDictionary new], }]; _minimumInvalidatedSection = NSNotFound; _preserveLayoutCacheOnInvalidateLayout = NO; } return self; } #pragma mark - UICollectionViewLayout - (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath { UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath]; id delegate = (id)self.collectionView.delegate; if ([delegate respondsToSelector:@selector(collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:)]) { return [delegate collectionView:self.collectionView layout:self customizedInitialLayoutAttributes:attributes atIndexPath:itemIndexPath]; } return attributes; } - (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath{ UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath]; id delegate = (id)self.collectionView.delegate; if ([delegate respondsToSelector:@selector(collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:)]) { return [delegate collectionView:self.collectionView layout:self customizedFinalLayoutAttributes:attributes atIndexPath:itemIndexPath]; } return attributes; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { IGAssertMainThread(); NSMutableArray *result = [NSMutableArray new]; const NSRange range = [self _rangeOfSectionsInRect:rect]; if (range.location == NSNotFound) { return nil; } for (NSInteger section = range.location; section < (NSInteger)NSMaxRange(range); section++) { const NSInteger itemCount = _sectionData[section].itemBounds.size(); // do not add headers if there are no items if (itemCount > 0 || self.showHeaderWhenEmpty) { for (NSString *elementKind in _supplementaryAttributesCache.allKeys) { NSIndexPath *indexPath = indexPathForSection(section); UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:elementKind atIndexPath:indexPath]; // do not add zero height headers/footers or headers/footers that are outside the rect const CGRect frame = attributes.frame; const CGRect intersection = CGRectIntersection(frame, rect); if (attributes && !CGRectIsEmpty(intersection) && CGRectGetLengthInDirection(frame, self.scrollDirection) > 0.0) { [result addObject:attributes]; } } } // add all cells within the rect, return early if it starts iterating outside for (NSInteger item = 0; item < itemCount; item++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; if (attributes && CGRectIntersectsRect(attributes.frame, rect)) { [result addObject:attributes]; } } } return result; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); IGParameterAssert(indexPath != nil); UICollectionViewLayoutAttributes *attributes = _attributesCache[indexPath]; if (attributes != nil) { return attributes; } if (indexPath == nil) { return nil; } // avoid OOB errors const NSInteger section = indexPath.section; const NSInteger item = indexPath.item; if (section >= (ssize_t)_sectionData.size() || item >= (ssize_t)_sectionData[section].itemBounds.size()) { return nil; } attributes = [[[self class] layoutAttributesClass] layoutAttributesForCellWithIndexPath:indexPath]; CGRect frame = _sectionData[indexPath.section].itemBounds[indexPath.item]; // Avoid setting frames with nan values if (isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height)) { IGFailAssert(@"IGListCollectionViewLayout encountered nan frame values for indexPath %@. Original frame: %@", indexPath, NSStringFromCGRect(frame)); return nil; } attributes.frame = frame; adjustZIndexForAttributes(attributes); _attributesCache[indexPath] = attributes; return attributes; } - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { IGAssertMainThread(); IGParameterAssert(indexPath != nil); UICollectionViewLayoutAttributes *attributes = _supplementaryAttributesCache[elementKind][indexPath]; if (attributes != nil) { return attributes; } // avoid OOB errors const NSInteger section = indexPath.section; if (section >= (ssize_t)_sectionData.size()) { return nil; } UICollectionView *collectionView = self.collectionView; const IGListSectionEntry entry = _sectionData[section]; const CGFloat minOffset = CGRectGetMinInDirection(entry.bounds, self.scrollDirection); CGRect frame = CGRectZero; if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) { frame = entry.headerBounds; if (self.stickyHeaders) { CGFloat offset = CGPointGetCoordinateInDirection(collectionView.contentOffset, self.scrollDirection) + self.topContentInset + self.stickyHeaderYOffset; if (section + 1 == (ssize_t)_sectionData.size()) { offset = MAX(minOffset, offset); } else { const CGFloat maxOffset = CGRectGetMinInDirection(_sectionData[section + 1].bounds, self.scrollDirection) - CGRectGetLengthInDirection(frame, self.scrollDirection); offset = MIN(MAX(minOffset, offset), maxOffset); } switch (self.scrollDirection) { case UICollectionViewScrollDirectionVertical: frame.origin.y = offset; break; case UICollectionViewScrollDirectionHorizontal: frame.origin.x = offset; break; default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, self.scrollDirection); } } } else if ([elementKind isEqualToString:UICollectionElementKindSectionFooter]) { frame = entry.footerBounds; } if (CGRectIsEmpty(frame)) { // Just like UICollectionViewFlowLayout, if the header/footer size is empty, do not not return an attribute. // If we did return something, calling [UICollectionView layoutAttributesForSupplementaryElementOfKind...] would too, // which could then crash if the UICollectionViewDelegate is not expecting to actually return a supplimentary view. return nil; } else { attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath]; attributes.frame = frame; adjustZIndexForAttributes(attributes); _supplementaryAttributesCache[elementKind][indexPath] = attributes; return attributes; } } - (CGSize)collectionViewContentSize { IGAssertMainThread(); const NSInteger sectionCount = _sectionData.size(); if (sectionCount == 0) { return CGSizeZero; } const IGListSectionEntry section = _sectionData[sectionCount - 1]; UICollectionView *collectionView = self.collectionView; const UIEdgeInsets contentInset = collectionView.ig_contentInset; switch (self.scrollDirection) { case UICollectionViewScrollDirectionVertical: return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, CGRectGetMaxY(section.bounds) + section.insets.bottom); case UICollectionViewScrollDirectionHorizontal: return CGSizeMake(CGRectGetMaxX(section.bounds) + section.insets.right, CGRectGetHeight(collectionView.bounds) - contentInset.top - contentInset.bottom); default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(UICollectionViewScrollDirection, self.scrollDirection); } } - (void)invalidateLayout { if (!_preserveLayoutCacheOnInvalidateLayout) { _minimumInvalidatedSection = 0; } [super invalidateLayout]; } - (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context { BOOL hasInvalidatedItemIndexPaths = NO; if ([context respondsToSelector:@selector(invalidatedItemIndexPaths)]) { hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0; } if (hasInvalidatedItemIndexPaths || [context invalidateEverything] || ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound) // if count changed and we don't have information on the minimum invalidated section || context.invalidateAllListAttributes) { // invalidates all _minimumInvalidatedSection = 0; } if (context.invalidateSupplementaryListAttributes) { [self _resetSupplementaryAttributesCache]; } [super invalidateLayoutWithContext:context]; } + (Class)invalidationContextClass { return [IGListCollectionViewLayoutInvalidationContext class]; } - (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds { const CGRect oldBounds = self.collectionView.bounds; IGListCollectionViewLayoutInvalidationContext *context = (IGListCollectionViewLayoutInvalidationContext *)[super invalidationContextForBoundsChange:newBounds]; context.invalidateSupplementaryListAttributes = YES; if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { context.invalidateAllListAttributes = YES; } return context; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { const CGRect oldBounds = self.collectionView.bounds; // always invalidate for size changes if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { return YES; } // if the y origin has changed, only invalidate when using sticky headers if (CGRectGetMinInDirection(newBounds, self.scrollDirection) != CGRectGetMinInDirection(oldBounds, self.scrollDirection)) { return self.stickyHeaders; } return NO; } - (void)prepareLayout { [self _calculateLayoutIfNeeded]; } #pragma mark - Public API - (void)setStickyHeaderYOffset:(CGFloat)stickyHeaderYOffset { IGAssertMainThread(); if (_stickyHeaderYOffset != stickyHeaderYOffset) { _stickyHeaderYOffset = stickyHeaderYOffset; IGListCollectionViewLayoutInvalidationContext *invalidationContext = [IGListCollectionViewLayoutInvalidationContext new]; invalidationContext.invalidateSupplementaryListAttributes = YES; [self invalidateLayoutWithContext:invalidationContext]; } } #pragma mark - Private API - (NSString *)_classNameForDelegate:(id)delegate sectionIndex:(NSInteger)section { NSString *const delegateClassString = NSStringFromClass(delegate.class); const BOOL isListAdapter = [delegateClassString isEqualToString:@"IGListAdapter"]; const BOOL isListAdapterProxy = [delegateClassString isEqualToString:@"IGListAdapterProxy"]; if (isListAdapter == NO && isListAdapterProxy == NO) { return delegateClassString; } const id forwardingObject = (isListAdapterProxy ? [(id)delegate forwardingTargetForSelector:@selector(collectionView:layout:insetForSectionAtIndex:)] : delegate); if ([forwardingObject isKindOfClass:IGListAdapter.class] == NO) { return NSStringFromClass([forwardingObject class]); } const id sectionController = [forwardingObject sectionControllerForSection:section]; return NSStringFromClass([sectionController class]); } - (void)_calculateLayoutIfNeeded { if (_minimumInvalidatedSection == NSNotFound) { return; } UICollectionView *collectionView = self.collectionView; id delegate = (id)collectionView.delegate; const NSInteger sectionCount = [collectionView numberOfSections]; const UIEdgeInsets contentInset = collectionView.ig_contentInset; const CGRect contentInsetAdjustedCollectionViewBounds = UIEdgeInsetsInsetRect(collectionView.bounds, contentInset); _sectionData.resize(sectionCount); CGFloat itemCoordInScrollDirection = 0.0; CGFloat itemCoordInFixedDirection = 0.0; CGFloat nextRowCoordInScrollDirection = 0.0; // union item frames and optionally the header to find a bounding box of the entire section CGRect rollingSectionBounds = CGRectZero; // populate last valid section information const NSInteger lastValidSection = _minimumInvalidatedSection - 1; if (lastValidSection >= 0 && lastValidSection < sectionCount) { itemCoordInScrollDirection = _sectionData[lastValidSection].lastItemCoordInScrollDirection; itemCoordInFixedDirection = _sectionData[lastValidSection].lastItemCoordInFixedDirection; nextRowCoordInScrollDirection = _sectionData[lastValidSection].lastNextRowCoordInScrollDirection; rollingSectionBounds = _sectionData[lastValidSection].bounds; } for (NSInteger section = _minimumInvalidatedSection; section < sectionCount; section++) { const NSInteger itemCount = [collectionView numberOfItemsInSection:section]; const BOOL itemsEmpty = itemCount == 0; const BOOL hideHeaderWhenItemsEmpty = itemsEmpty && !self.showHeaderWhenEmpty; _sectionData[section].itemBounds = std::vector(itemCount); const CGSize headerSize = [delegate collectionView:collectionView layout:self referenceSizeForHeaderInSection:section]; const CGSize footerSize = [delegate collectionView:collectionView layout:self referenceSizeForFooterInSection:section]; const UIEdgeInsets insets = [delegate collectionView:collectionView layout:self insetForSectionAtIndex:section]; const CGFloat lineSpacing = [delegate collectionView:collectionView layout:self minimumLineSpacingForSectionAtIndex:section]; const CGFloat interitemSpacing = [delegate collectionView:collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section]; const CGSize paddedCollectionViewSize = UIEdgeInsetsInsetRect(contentInsetAdjustedCollectionViewBounds, insets).size; const UICollectionViewScrollDirection fixedDirection = self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal; const CGFloat paddedLengthInFixedDirection = CGSizeGetLengthInDirection(paddedCollectionViewSize, fixedDirection); const CGFloat headerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(headerSize, self.scrollDirection); const CGFloat footerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(footerSize, self.scrollDirection); const BOOL headerExists = headerLengthInScrollDirection > 0; const BOOL footerExists = footerLengthInScrollDirection > 0; // start the section accounting for the header size // header length in scroll direction is subtracted from the sectionBounds when calculating the header bounds after items are done // this bumps the first row of items over enough to make room for the header itemCoordInScrollDirection += headerLengthInScrollDirection; nextRowCoordInScrollDirection += headerLengthInScrollDirection; // add the leading inset in fixed direction in case the section falls on the same row as the previous // if the section is newlined then the coord in fixed direction is reset itemCoordInFixedDirection += UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); // the farthest in the fixed direction the frame of an item in this section can go const CGFloat maxCoordinateInFixedDirection = CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection) - UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); for (NSInteger item = 0; item < itemCount; item++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; // Following method subsequentally calls -layoutAttributesForItemAtIndexPath: and caches attributes that are not ready yet (we only calculate them at the end of this for loop) // This results in _attributesCache[indexPath] being set to incorrect value. If we end up calling prepareLayout in response to frame change we const CGSize size = [delegate collectionView:collectionView layout:self sizeForItemAtIndexPath:indexPath]; IGAssert(CGSizeGetLengthInDirection(size, fixedDirection) <= paddedLengthInFixedDirection || fabs(CGSizeGetLengthInDirection(size, fixedDirection) - paddedLengthInFixedDirection) < FLT_EPSILON, @"%@ of item %li in section %li (%.0f pt) must be less than or equal to container (%.0f pt) accounting for section insets %@. Delegate class: %@", self.scrollDirection == UICollectionViewScrollDirectionVertical ? @"Width" : @"Height", (long)item, (long)section, CGSizeGetLengthInDirection(size, fixedDirection), CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection), NSStringFromUIEdgeInsets(insets), [self _classNameForDelegate:delegate sectionIndex:section]); CGFloat itemLengthInFixedDirection = MIN(CGSizeGetLengthInDirection(size, fixedDirection), paddedLengthInFixedDirection); // if the origin and length in fixed direction of the item busts the size of the container // or if this is the first item and the header has a non-zero size // newline to the next row and reset // define epsilon to avoid float overflow issue const CGFloat epsilon = 1.0; if (itemCoordInFixedDirection + itemLengthInFixedDirection > maxCoordinateInFixedDirection + epsilon || (item == 0 && headerExists)) { itemCoordInScrollDirection = nextRowCoordInScrollDirection; itemCoordInFixedDirection = UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); // if newlining, always append line spacing unless its the very first item of the section if (item > 0) { itemCoordInScrollDirection += lineSpacing; } } const CGFloat distanceToEdge = paddedLengthInFixedDirection - (itemCoordInFixedDirection + itemLengthInFixedDirection); if (self.stretchToEdge && distanceToEdge > 0 && distanceToEdge <= epsilon) { itemLengthInFixedDirection = paddedLengthInFixedDirection - itemCoordInFixedDirection; } const CGRect rawFrame = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? CGRectMake(itemCoordInFixedDirection, itemCoordInScrollDirection + insets.top, itemLengthInFixedDirection, size.height) : CGRectMake(itemCoordInScrollDirection + insets.left, itemCoordInFixedDirection, size.width, itemLengthInFixedDirection); const CGRect frame = IGListRectIntegralScaled(rawFrame); _sectionData[section].itemBounds[item] = frame; // track the max size of the row to find the coord of the next row, adjust for leading inset while iterating items nextRowCoordInScrollDirection = MAX(CGRectGetMaxInDirection(frame, self.scrollDirection) - UIEdgeInsetsLeadingInsetInDirection(insets, self.scrollDirection), nextRowCoordInScrollDirection); // increase the rolling coord in fixed direction appropriately and add item spacing for all items on the same row itemCoordInFixedDirection += itemLengthInFixedDirection + interitemSpacing; // union the rolling section bounds if (item == 0) { rollingSectionBounds = frame; } else { rollingSectionBounds = CGRectUnion(rollingSectionBounds, frame); } } const CGRect headerBounds = self.scrollDirection == UICollectionViewScrollDirectionVertical ? CGRectMake(insets.left, itemsEmpty ? CGRectGetMaxY(rollingSectionBounds) : CGRectGetMinY(rollingSectionBounds) - headerSize.height, paddedLengthInFixedDirection, hideHeaderWhenItemsEmpty ? 0 : headerSize.height) : CGRectMake(itemsEmpty ? CGRectGetMaxX(rollingSectionBounds) : CGRectGetMinX(rollingSectionBounds) - headerSize.width, insets.top, hideHeaderWhenItemsEmpty ? 0 : headerSize.width, paddedLengthInFixedDirection); _sectionData[section].headerBounds = headerBounds; if (itemsEmpty) { rollingSectionBounds = headerBounds; } const CGRect footerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? CGRectMake(insets.left, CGRectGetMaxY(rollingSectionBounds), paddedLengthInFixedDirection, hideHeaderWhenItemsEmpty ? 0 : footerSize.height) : CGRectMake(CGRectGetMaxX(rollingSectionBounds) + insets.right, insets.top, hideHeaderWhenItemsEmpty ? 0 : footerSize.width, paddedLengthInFixedDirection); _sectionData[section].footerBounds = footerBounds; // union the header before setting the bounds of the section // only do this when the header has a size, otherwise the union stretches to box empty space if (headerExists) { rollingSectionBounds = CGRectUnion(rollingSectionBounds, headerBounds); } if (footerExists) { rollingSectionBounds = CGRectUnion(rollingSectionBounds, footerBounds); } _sectionData[section].bounds = rollingSectionBounds; _sectionData[section].insets = insets; // bump the coord for the next section with the right insets itemCoordInFixedDirection += UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); // find the farthest point in the section and add the trailing inset to find the next row's coord nextRowCoordInScrollDirection = MAX(nextRowCoordInScrollDirection, CGRectGetMaxInDirection(rollingSectionBounds, self.scrollDirection) + UIEdgeInsetsTrailingInsetInDirection(insets, self.scrollDirection)); // keep track of coordinates for partial invalidation _sectionData[section].lastItemCoordInScrollDirection = itemCoordInScrollDirection; _sectionData[section].lastItemCoordInFixedDirection = itemCoordInFixedDirection; _sectionData[section].lastNextRowCoordInScrollDirection = nextRowCoordInScrollDirection; } // Reason we are purging attributes at the end is because in some circumstances calling // -[delegate collectionView: layout: sizeForItemAtIndexPath:] results in creating the cache with incorrect values // See the comment next to the call for more information [_attributesCache removeAllObjects]; [self _resetSupplementaryAttributesCache]; _minimumInvalidatedSection = NSNotFound; } - (NSRange)_rangeOfSectionsInRect:(CGRect)rect { NSRange result = NSMakeRange(NSNotFound, 0); const NSInteger sectionCount = _sectionData.size(); for (NSInteger section = 0; section < sectionCount; section++) { IGListSectionEntry entry = _sectionData[section]; if (entry.isValid() && CGRectIntersectsRect(entry.bounds, rect)) { const NSRange sectionRange = NSMakeRange(section, 1); if (result.location == NSNotFound) { result = sectionRange; } else { result = NSUnionRange(result, sectionRange); } } } return result; } - (void)_resetSupplementaryAttributesCache { [_supplementaryAttributesCache enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull attributesCache, BOOL * _Nonnull stop) { [attributesCache removeAllObjects]; }]; } #pragma mark - Minimum Invalidated Section - (void)didModifySection:(NSInteger)modifiedSection { _minimumInvalidatedSection = IGListMergeMinimumInvalidatedSection(_minimumInvalidatedSection, modifiedSection); } @end ================================================ FILE: Source/IGListKit/IGListCollectionViewLayoutCompatible.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** A protocol for layouts that defines interaction with an IGListCollectionView, for recieving updated section indexes. */ NS_SWIFT_NAME(ListCollectionViewLayoutCompatible) @protocol IGListCollectionViewLayoutCompatible /** Called to notify the layout that a specific section was modified before invalidation. This can be used to optimize layout re-calculation. @note When updating a collection view (ex: calling `-insertSections`), `-invalidateLayoutWithContext` gets called on the layout object. However, the invalidation context doesn't provide details on which index paths are being modified, which typically forces a full layout re-calculation. Layouts can use this method to keep track of which section actually needs to be updated on the following `-invalidateLayoutWithContext`. See `IGListCollectionView`. @param modifiedSection The section that was modified. */ - (void)didModifySection:(NSInteger)modifiedSection; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListCollectionViewLayoutInvalidationContext.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** The default invalidation context class used by IGListCollectionViewLayout. */ NS_SWIFT_NAME(ListCollectionViewLayoutInvalidationContext) @interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext /** False by default. If true, supplementary list item attributes (headers and footers) will be invalidated. */ @property (nonatomic, assign) BOOL invalidateSupplementaryListAttributes; /** False by default. If true, all list item attributes will be invalidated. */ @property (nonatomic, assign) BOOL invalidateAllListAttributes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListCollectionViewLayoutInvalidationContext.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListCollectionViewLayoutInvalidationContext.h" @implementation IGListCollectionViewLayoutInvalidationContext @end ================================================ FILE: Source/IGListKit/IGListDisplayDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** Implement this protocol to receive display events for a section controller when it is on screen. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListDisplayDelegate) @protocol IGListDisplayDelegate /** Tells the delegate that the specified section controller is about to be displayed. @param listAdapter The list adapter for the section controller. @param sectionController The section controller about to be displayed. */ - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController; /** Tells the delegate that the specified section controller is no longer being displayed. @param listAdapter The list adapter for the section controller. @param sectionController The section controller that is no longer displayed. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController; /** Tells the delegate that a cell in the specified list is about to be displayed. @param listAdapter The list adapter in which the cell will display. @param sectionController The section controller that is displaying the cell. @param cell The cell about to be displayed. @param index The index of the cell in the section. */ - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index; /** Tells the delegate that a cell in the specified list is no longer being displayed. @param listAdapter The list adapter in which the cell was displayed. @param sectionController The section controller that is no longer displaying the cell. @param cell The cell that is no longer displayed. @param index The index of the cell in the section. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListGenericSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #if !__has_include() #import "IGListSectionController.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** This class adds a helper layer to `IGListSectionController` to automatically store a generic object in `didUpdateToObject:`. */ NS_SWIFT_NAME(ListGenericSectionController) @interface IGListGenericSectionController<__covariant ObjectType> : IGListSectionController /** The object mapped to this section controller. Matches the object provided in `[IGListAdapterDataSource listAdapter:sectionControllerForObject:]` when this section controller was created and returned. @note This object is briefly `nil` between initialization and the first call to `didUpdateToObject:`. After that, it is safe to assume that this is non-`nil`. For this reason, we bridge it to Swift as an implicitly-unwrapped Optional, so that idiomatic IGListKit code is not forced to handle nullability with explicit `as!` or `fatalError`. */ @property (nonatomic, strong, null_unspecified, readonly) ObjectType object; /** Updates the section controller to a new object. @param object The object mapped to this section controller. @note This `IGListSectionController` subclass sets its object in this method, so any overrides **must call super**. */ - (void)didUpdateToObject:(ObjectType)object NS_REQUIRES_SUPER; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListGenericSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListGenericSectionController.h" @implementation IGListGenericSectionController - (void)didUpdateToObject:(id)object { _object = object; } @end ================================================ FILE: Source/IGListKit/IGListKit.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #if !__has_include() #import "IGListCompatibility.h" #else #import #endif /** * Project version number for IGListKit. */ FOUNDATION_EXPORT double IGListKitVersionNumber; /** * Project version string for IGListKit. */ FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; #if TARGET_OS_EMBEDDED || TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST // iOS and tvOS only: #if !__has_include() #import "IGListAdapter.h" #import "IGListAdapterDataSource.h" #import "IGListAdapterDelegate.h" #import "IGListAdapterDelegateAnnouncer.h" #import "IGListAdapterUpdateListener.h" #import "IGListAdapterUpdater.h" #import "IGListAdapterUpdaterDelegate.h" #import "IGListBatchContext.h" #import "IGListBindable.h" #import "IGListBindingSectionController.h" #import "IGListBindingSectionControllerDataSource.h" #import "IGListBindingSectionControllerSelectionDelegate.h" #import "IGListBindingSingleSectionController.h" #import "IGListCollectionContext.h" #import "IGListCollectionView.h" #import "IGListCollectionViewLayout.h" #import "IGListCollectionViewLayoutInvalidationContext.h" #import "IGListDisplayDelegate.h" #import "IGListGenericSectionController.h" #import "IGListCollectionViewDelegateLayout.h" #import "IGListReloadDataUpdater.h" #import "IGListScrollDelegate.h" #import "IGListSectionController.h" #import "IGListSingleSectionController.h" #import "IGListSupplementaryViewSource.h" #import "IGListTransitionData.h" #import "IGListTransitionDelegate.h" #import "IGListUpdatingDelegate.h" #import "IGListWorkingRangeDelegate.h" #import "UIViewController+IGListAdapter.h" #else #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #endif #endif // Shared (iOS, tvOS, macOS compatible): #if !__has_include() #import "IGListAssert.h" #import "IGListBatchUpdateData.h" #import "IGListDiff.h" #import "IGListDiffable.h" #import "IGListExperiments.h" #import "IGListIndexPathResult.h" #import "IGListIndexSetResult.h" #import "IGListMoveIndex.h" #import "IGListMoveIndexPath.h" #import "NSNumber+IGListDiffable.h" #import "NSString+IGListDiffable.h" #else #import #import #import #import #import #import #import #import #import #import #import #endif ================================================ FILE: Source/IGListKit/IGListReloadDataUpdater.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListUpdatingDelegate.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** An `IGListReloadDataUpdater` is a concrete type that conforms to `IGListUpdatingDelegate`. It is an out-of-box updater for `IGListAdapter` objects to use. @note This updater performs simple, synchronous updates using `-[UICollectionView reloadData]`. */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_NAME(ListReloadDataUpdater) @interface IGListReloadDataUpdater : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListReloadDataUpdater.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListReloadDataUpdater.h" @implementation IGListReloadDataUpdater { BOOL _isInDataUpdateBlock; } #pragma mark - IGListUpdatingDelegate - (NSPointerFunctions *)objectLookupPointerFunctions { return [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality]; } - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(nullable IGListUpdatingCompletion)completion { IGListTransitionData *sectionData = sectionDataBlock ? sectionDataBlock() : nil; if (sectionData != nil && applySectionDataBlock != nil) { _isInDataUpdateBlock = YES; applySectionDataBlock(sectionData); _isInDataUpdateBlock = NO; } [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; if (completion) { completion(YES); } } - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated itemUpdates:(IGListItemUpdateBlock)itemUpdates completion:(IGListUpdatingCompletion)completion { itemUpdates(); [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; if (completion) { completion(YES); } } - (void)performDataSourceChange:(IGListDataSourceChangeBlock)block { // A `UICollectionView` dataSource change will automatically invalidate // its data, so no need to do anything else. block(); } - (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion { reloadUpdateBlock(); [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; if (completion) { completion(YES); } } - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { [self _synchronousReloadDataWithCollectionView:collectionView]; } - (void)_synchronousReloadDataWithCollectionView:(UICollectionView *)collectionView { [collectionView reloadData]; [collectionView layoutIfNeeded]; } - (BOOL)isInDataUpdateBlock { return _isInDataUpdateBlock; } - (void)willCrashWithCollectionView:(UICollectionView *)collectionView sectionControllerClass:(Class)sectionControllerClass {} @end ================================================ FILE: Source/IGListKit/IGListScrollDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** Implement this protocol to receive display events for a section controller when it is on screen. */ NS_SWIFT_NAME(ListScrollDelegate) @protocol IGListScrollDelegate /** Tells the delegate that the section controller was scrolled on screen. @param listAdapter The list adapter whose collection view was scrolled. @param sectionController The visible section controller that was scrolled. */ - (void)listAdapter:(IGListAdapter *)listAdapter didScrollSectionController:(IGListSectionController *)sectionController; /** Tells the delegate that the section controller will be dragged on screen. @param listAdapter The list adapter whose collection view will drag. @param sectionController The visible section controller that will drag. */ - (void)listAdapter:(IGListAdapter *)listAdapter willBeginDraggingSectionController:(IGListSectionController *)sectionController; /** Tells the delegate that the section controller did end dragging on screen. @param listAdapter The list adapter whose collection view ended dragging. @param sectionController The visible section controller that ended dragging. @param decelerate 'Yes' if the scrolling movement will continue, but decelerate, after a touch-up gesture during a dragging operation. If the value is 'No', scrolling stops immediately upon touch-up. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDraggingSectionController:(IGListSectionController *)sectionController willDecelerate:(BOOL)decelerate; @optional /** Tells the delegate that the section controller did end decelerating on screen. @param listAdapter The list adapter whose collection view ended decelerating. @param sectionController The visible section controller that ended decelerating. @note This method is `@optional` until the next breaking-change release. */ - (void)listAdapter:(IGListAdapter *)listAdapter didEndDeceleratingSectionController:(IGListSectionController *)sectionController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListCollectionContext.h" #import "IGListDisplayDelegate.h" #import "IGListScrollDelegate.h" #import "IGListSupplementaryViewSource.h" #import "IGListTransitionDelegate.h" #import "IGListWorkingRangeDelegate.h" #else #import #import #import #import #import #import #endif NS_ASSUME_NONNULL_BEGIN /** The base class for section controllers used in a list. This class is intended to be subclassed. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListSectionController) @interface IGListSectionController : NSObject /** Returns the number of items in the section. @return A count of items in the list. @note The count returned is used to drive the number of cells displayed for this section controller. The default implementation returns 1. **Calling super is not required.** */ - (NSInteger)numberOfItems; /** The specific size for the item at the specified index. @param index The row index of the item. @return The size for the item at index. @note The returned size is not guaranteed to be used. The implementation may query sections for their layout information at will, or use its own layout metrics. For example, consider a dynamic-text sized list versus a fixed height-and-width grid. The former will ask each section for a size, and the latter will likely not. The default implementation returns size zero. **Calling super is not required.** */ - (CGSize)sizeForItemAtIndex:(NSInteger)index; /** Return a dequeued cell for a given index. @param index The index of the requested row. @return A configured `UICollectionViewCell` subclass. @note This is your opportunity to do any cell setup and configuration. The infrastructure requests a cell when it will be used on screen. You should never allocate new cells in this method, instead use the provided adapter to call one of the dequeue methods on the IGListCollectionContext. The default implementation will assert. **You must override this method without calling super.** @warning Don't call this method to obtain a reference to currently dequeued cells: a new cell will be dequeued and returned, rather than the existing cell that you may have intended to retrieve. Instead, you can call `-cellForItemAtIndex:sectionController:` on `IGListCollectionContext` to obtain active cell references. */ - (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index NS_SWIFT_UI_ACTOR; /** Updates the section controller to a new object. @param object The object mapped to this section controller. @note When this method is called, all available contexts and configurations have been set for the section controller. This method will only be called when the object instance has changed, including from `nil` or a previous object. **Calling super is not required.** */ - (void)didUpdateToObject:(id)object; /** Asks the section controller if the cell at the specified index path should be selected @param index The index of cell to be selected. @note The default implementation returns YES. **Calling super is not required.** */ - (BOOL)shouldSelectItemAtIndex:(NSInteger)index; /** Asks the section controller if the cell at the specified index path should be deselected @param index The index of cell to be deselected. @note The default implementation returns YES. **Calling super is not required.** */ - (BOOL)shouldDeselectItemAtIndex:(NSInteger)index; /** Tells the section controller that the cell at the specified index path was selected. @param index The index of the selected cell. @note The default implementation does nothing. **Calling super is not required.** */ - (void)didSelectItemAtIndex:(NSInteger)index; /** Tells the section controller that the cell at the specified index path was deselected. @param index The index of the deselected cell. @note The default implementation does nothing. **Calling super is not required.** */ - (void)didDeselectItemAtIndex:(NSInteger)index; /** Tells the section controller that the cell at the specified index path was highlighted. @param index The index of the highlighted cell. @note The default implementation does nothing. **Calling super is not required.** */ - (void)didHighlightItemAtIndex:(NSInteger)index; /** Tells the section controller that the cell at the specified index path was unhighlighted. @param index The index of the unhighlighted cell. @note The default implementation does nothing. **Calling super is not required.** */ - (void)didUnhighlightItemAtIndex:(NSInteger)index; /** Tells the section controller that the cell has requested a menu configuration. @param index The index of the cell that requested the menu. @param point The point of the tap on the cell. @return An object that conforms to `UIContextMenuConfiguration` @note The default implementation does nothing. **Calling super is not required.** */ - (UIContextMenuConfiguration * _Nullable)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos); /** Identifies whether an object can be moved through interactive reordering. @param index The index of the object in the list. @return `YES` if the object is allowed to move, otherwise `NO`. @note Interactive reordering is supported both for items within a single section, as well as for reordering sections themselves when sections contain only one item. The default implementation returns false. */ - (BOOL)canMoveItemAtIndex:(NSInteger)index; /** Notifies the section that a list object should move within a section as the result of interactive reordering. @param sourceIndex The starting index of the object. @param destinationIndex The ending index of the object. @note this method must be implemented if interactive reordering is enabled. */ - (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex NS_AVAILABLE_IOS(9_0); /** The view controller housing the adapter that created this section controller. @note Use this view controller to push, pop, present, or do other custom transitions. @warning It is considered very bad practice to cast this to a known view controller and call methods on it other than for navigations and transitions. */ @property (nonatomic, weak, nullable, readonly) UIViewController *viewController; /** A context object for interacting with the collection. Use this property for accessing the collection size, dequeuing cells, reloading, inserting, deleting, etc. @note When created outside of `-listAdapter:sectionControllerForObject:`, this object is temporarily `nil` after initialization. We bridge it to Swift as an implicitly-unwrapped Optional, so that idiomatic IGListKit code is not forced to handle nullability with explicit `as!` or `fatalError`, as using a non-`nil` instance of this object is essential for dequeueing cells. */ @property (nonatomic, weak, null_unspecified, readonly) id collectionContext; /** Returns the section within the list for this section controller. @note This value also relates to the section within a `UICollectionView` that this section controller's cells belong. It also relates to the `-[NSIndexPath section]` value for individual cells within the collection view. */ @property (nonatomic, assign, readonly) NSInteger section; /** Returns `YES` if the section controller is the first section in the list, `NO` otherwise. */ @property (nonatomic, assign, readonly) BOOL isFirstSection; /** Returns `YES` if the section controller is the last section in the list, `NO` otherwise. */ @property (nonatomic, assign, readonly) BOOL isLastSection; /** The margins used to lay out content in the section controller. @see `-[UICollectionViewFlowLayout sectionInset]`. */ @property (nonatomic, assign) UIEdgeInsets inset; /** The minimum spacing to use between rows of items. @see `-[UICollectionViewFlowLayout minimumLineSpacing]`. */ @property (nonatomic, assign) CGFloat minimumLineSpacing; /** The minimum spacing to use between items in the same row. @see `-[UICollectionViewFlowLayout minimumInteritemSpacing]`. */ @property (nonatomic, assign) CGFloat minimumInteritemSpacing; /** The supplementary view source for the section controller. Can be `nil`. @return An object that conforms to `IGListSupplementaryViewSource` or `nil`. @note You may wish to return `self` if your section controller implements this protocol. */ @property (nonatomic, weak, nullable) id supplementaryViewSource; /** An object that handles display events for the section controller. Can be `nil`. @return An object that conforms to `IGListDisplayDelegate` or `nil`. @note You may wish to return `self` if your section controller implements this protocol. */ @property (nonatomic, weak, nullable) id displayDelegate; /** An object that handles working range events for the section controller. Can be `nil`. @return An object that conforms to `IGListWorkingRangeDelegate` or `nil`. @note You may wish to return `self` if your section controller implements this protocol. */ @property (nonatomic, weak, nullable) id workingRangeDelegate; /** An object that handles scroll events for the section controller. Can be `nil`. @return An object that conforms to `IGListScrollDelegate` or `nil`. @note You may wish to return `self` if your section controller implements this protocol. */ @property (nonatomic, weak, nullable) id scrollDelegate; /** An object that handles transition events for the section controller. Can be `nil`. @return An object that conforms to `IGListTransitionDelegat` or `nil`. @note You may wish to return `self` if your section controller implements this protocol. */ @property (nonatomic, weak, nullable) id transitionDelegate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListSectionControllerInternal.h" #if !__has_include() #import "IGListAssert.h" #import "IGListMacros.h" #else #import #import #endif static NSString * const kIGListSectionControllerThreadKey = @"kIGListSectionControllerThreadKey"; @interface IGListSectionControllerThreadContext : NSObject @property (nonatomic, weak) UIViewController *viewController; @property (nonatomic, weak) id collectionContext; @end @implementation IGListSectionControllerThreadContext @end static NSMutableArray *threadContextStack(void) { IGAssertMainThread(); NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; NSMutableArray *stack = threadDictionary[kIGListSectionControllerThreadKey]; if (stack == nil) { stack = [NSMutableArray new]; threadDictionary[kIGListSectionControllerThreadKey] = stack; } return stack; } void IGListSectionControllerPushThread(UIViewController *viewController, id collectionContext) { IGListSectionControllerThreadContext *context = [IGListSectionControllerThreadContext new]; context.viewController = viewController; context.collectionContext = collectionContext; [threadContextStack() addObject:context]; } void IGListSectionControllerPopThread(void) { NSMutableArray *stack = threadContextStack(); IGAssert(stack.count > 0, @"IGListSectionController thread stack is empty"); [stack removeLastObject]; } @implementation IGListSectionController - (instancetype)init { if (self = [super init]) { IGListSectionControllerThreadContext *context = [threadContextStack() lastObject]; _viewController = context.viewController; _collectionContext = context.collectionContext; if (_collectionContext == nil) { IGLKLog(@"Warning: Creating %@ outside of -[IGListAdapterDataSource listAdapter:sectionControllerForObject:]. Collection context and view controller will be set later.", NSStringFromClass([self class])); } _minimumInteritemSpacing = 0.0; _minimumLineSpacing = 0.0; _inset = UIEdgeInsetsZero; _section = NSNotFound; } return self; } - (NSInteger)numberOfItems { return 1; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return CGSizeZero; } - (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { IGFailAssert(@"Section controller %@ must override %s:", self, __PRETTY_FUNCTION__); return nil; } - (void)didUpdateToObject:(id)object {} - (BOOL)shouldSelectItemAtIndex:(NSInteger)index { return YES; } - (BOOL)shouldDeselectItemAtIndex:(NSInteger)index { return YES; } - (void)didSelectItemAtIndex:(NSInteger)index {} - (void)didDeselectItemAtIndex:(NSInteger)index {} - (void)didHighlightItemAtIndex:(NSInteger)index {} - (void)didUnhighlightItemAtIndex:(NSInteger)index {} - (UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point { return nil; } - (BOOL)canMoveItemAtIndex:(NSInteger)index { return NO; } - (BOOL)canMoveItemAtIndex:(NSInteger)sourceItemIndex toIndex:(NSInteger)destinationItemIndex { return [self canMoveItemAtIndex:sourceItemIndex]; } - (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex { IGFailAssert(@"Section controller %@ must override %s if interactive reordering is enabled.", self, __PRETTY_FUNCTION__); } - (void)willDisplayCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter { [self.displayDelegate listAdapter:listAdapter willDisplaySectionController:self cell:cell atIndex:index]; } - (void)didEndDisplayingCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter { [self.displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:self cell:cell atIndex:index]; } - (void)willDisplaySectionControllerWithListAdapter:(IGListAdapter *)listAdapter { [self.displayDelegate listAdapter:listAdapter willDisplaySectionController:self]; } - (void)didEndDisplayingSectionControllerWithListAdapter:(IGListAdapter *)listAdapter { [self.displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:self]; } @end ================================================ FILE: Source/IGListKit/IGListSingleSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListSectionController.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** A block used to configure cells. @param item The model with which to configure the cell. @param cell The cell to configure. */ NS_SWIFT_NAME(ListSingleSectionCellConfigureBlock) typedef void (^IGListSingleSectionCellConfigureBlock)(id item, __kindof UICollectionViewCell *cell); /** A block that returns the size for the cell given the collection context. @param item The model for the section. @param collectionContext The collection context for the section. @return The size for the cell. */ NS_SWIFT_NAME(ListSingleSectionCellSizeBlock) typedef CGSize (^IGListSingleSectionCellSizeBlock)(id item, id _Nullable collectionContext); @class IGListSingleSectionController; /** A delegate that can receive selection events on an `IGListSingleSectionController`. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListSingleSectionControllerDelegate) @protocol IGListSingleSectionControllerDelegate /** Tells the delegate that the section controller was selected. @param sectionController The section controller that was selected. @param object The model for the given section. */ - (void)didSelectSectionController:(IGListSingleSectionController *)sectionController withObject:(id)object; @optional /** Tells the delegate that the section controller was deselected. @param sectionController The section controller that was deselected. @param object The model for the given section. @note Method is `@optional` until the 4.0.0 release where it will become required. */ - (void)didDeselectSectionController:(IGListSingleSectionController *)sectionController withObject:(id)object; @end /** This section controller is meant to make building simple, single-cell lists easier. By providing the type of cell, a block to configure the cell, and a block to return the size of a cell, you can use an `IGListAdapter`-powered list with a simpler architecture. */ NS_SWIFT_NAME(ListSingleSectionController) @interface IGListSingleSectionController : IGListSectionController /** Creates a new section controller for a given cell type that will always have only one cell when present in a list. @param cellClass The `UICollectionViewCell` subclass for the single cell. @param configureBlock A block that configures the cell with the item given to the section controller. @param sizeBlock A block that returns the size for the cell given the collection context. @return A new section controller. @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! */ - (instancetype)initWithCellClass:(Class)cellClass configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; /** Creates a new section controller for a given nib name and bundle that will always have only one cell when present in a list. @param nibName The name of the nib file for the single cell. @param bundle The bundle in which to search for the nib file. If `nil`, this method looks for the file in the main bundle. @param configureBlock A block that configures the cell with the item given to the section controller. @param sizeBlock A block that returns the size for the cell given the collection context. @return A new section controller. @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! */ - (instancetype)initWithNibName:(NSString *)nibName bundle:(nullable NSBundle *)bundle configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; /** Creates a new section controller for a given storyboard cell identifier that will always have only one cell when present in a list. @param identifier The identifier of the cell prototype in storyboard. @param configureBlock A block that configures the cell with the item given to the section controller. @param sizeBlock A block that returns the size for the cell given the collection context. @return A new section controller. @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! */ - (instancetype)initWithStoryboardCellIdentifier:(NSString *)identifier configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; /** An optional delegate that handles selection and deselection. */ @property (nonatomic, weak, nullable) id selectionDelegate; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListSingleSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListSingleSectionController.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif @interface IGListSingleSectionController () @property (nonatomic, strong, readonly) NSString *nibName; @property (nonatomic, strong, readonly) NSBundle *bundle; @property (nonatomic, strong, readonly) NSString *identifier; @property (nonatomic, strong, readonly) Class cellClass; @property (nonatomic, strong, readonly) IGListSingleSectionCellConfigureBlock configureBlock; @property (nonatomic, strong, readonly) IGListSingleSectionCellSizeBlock sizeBlock; @property (nonatomic, strong) id item; @end @implementation IGListSingleSectionController - (instancetype)initWithCellClass:(Class)cellClass configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { IGParameterAssert(cellClass != nil); IGParameterAssert(configureBlock != nil); IGParameterAssert(sizeBlock != nil); if (self = [super init]) { _cellClass = cellClass; _configureBlock = [configureBlock copy]; _sizeBlock = [sizeBlock copy]; } return self; } - (instancetype)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { IGParameterAssert(nibName != nil); IGParameterAssert(configureBlock != nil); IGParameterAssert(sizeBlock != nil); if (self = [super init]) { _nibName = [nibName copy]; _bundle = bundle; _configureBlock = [configureBlock copy]; _sizeBlock = [sizeBlock copy]; } return self; } - (instancetype)initWithStoryboardCellIdentifier:(NSString *)identifier configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { IGParameterAssert(identifier.length > 0); IGParameterAssert(configureBlock != nil); IGParameterAssert(sizeBlock != nil); if (self = [super init]) { _identifier = [identifier copy]; _configureBlock = [configureBlock copy]; _sizeBlock = [sizeBlock copy]; } return self; } #pragma mark - IGListSectionController Overrides - (NSInteger)numberOfItems { return 1; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return self.sizeBlock(self.item, self.collectionContext); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { IGParameterAssert(index == 0); id cell; id collectionContext = self.collectionContext; if ([self.nibName length] > 0) { cell = [collectionContext dequeueReusableCellWithNibName:self.nibName bundle:self.bundle forSectionController:self atIndex:index]; } else if ([self.identifier length] > 0) { cell = [collectionContext dequeueReusableCellFromStoryboardWithIdentifier:self.identifier forSectionController:self atIndex:index]; } else { cell = [collectionContext dequeueReusableCellOfClass:self.cellClass forSectionController:self atIndex:index]; } self.configureBlock(self.item, cell); return cell; } - (void)didUpdateToObject:(id)object { self.item = object; } - (void)didSelectItemAtIndex:(NSInteger)index { [self.selectionDelegate didSelectSectionController:self withObject:self.item]; } - (void)didDeselectItemAtIndex:(NSInteger)index { if ([self.selectionDelegate respondsToSelector:@selector(didDeselectSectionController:withObject:)]) { [self.selectionDelegate didDeselectSectionController:self withObject:self.item]; } } @end ================================================ FILE: Source/IGListKit/IGListSupplementaryViewSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /** Conform to this protocol to provide information about a list's supplementary views. This data is used in `IGListAdapter` which then configures and maintains a `UICollectionView`. The supplementary API reflects that in `UICollectionView`, `UICollectionViewLayout`, and `UICollectionViewDataSource`. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListSupplementaryViewSource) @protocol IGListSupplementaryViewSource /** Asks the SupplementaryViewSource for an array of supported element kinds. @return An array of element kind strings that the supplementary source handles. */ - (NSArray *)supportedElementKinds; /** Asks the SupplementaryViewSource for a configured supplementary view for the specified kind and index. @param elementKind The kind of supplementary view being requested @param index The index for the supplementary view being requested. @note This is your opportunity to do any supplementary view setup and configuration. @warning You should never allocate new views in this method. Instead deque a view from the `IGListCollectionContext`. */ - (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index; /** Asks the SupplementaryViewSource for the size of a supplementary view for the given kind and index path. @param elementKind The kind of supplementary view. @param index The index of the requested view. @return The size for the supplementary view. */ - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListTransitionData.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN @class IGListSectionController; /** Container object that holds the data needed for an update. */ IGLK_SUBCLASSING_RESTRICTED NS_SWIFT_NAME(ListTransitionData) @interface IGListTransitionData : NSObject - (instancetype)initFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects toSectionControllers:(NSArray *)toSectionControllers NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; /// The previous objects in the collection view. Objects must conform to `IGListDiffable`. @property (nonatomic, copy, readonly) NSArray *fromObjects; /// The new objects in the collection view. Objects must conform to `IGListDiffable`. @property (nonatomic, copy, readonly) NSArray *toObjects; /// The section controllers corresponding to the `toObjects` @property (nonatomic, copy, readonly) NSArray *toSectionControllers; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListTransitionData.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTransitionData.h" @implementation IGListTransitionData - (instancetype)initFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects toSectionControllers:(NSArray *)toSectionControllers { if (self = [super init]) { _fromObjects = [fromObjects copy]; _toObjects = [toObjects copy]; _toSectionControllers = [toSectionControllers copy]; } return self; } @end ================================================ FILE: Source/IGListKit/IGListTransitionDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /** Conform to `IGListTransitionDelegate` to provide customized layout information for a collection view. */ @protocol IGListTransitionDelegate /** Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view. @param listAdapter The adapter controlling the list. @param attributes The starting layout information for an item being inserted into the collection view. @param sectionController The section controller to perform the transition on. @param index The index of the item being inserted. */ - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; /** Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view. @param listAdapter The adapter controlling the list. @param attributes The final layout information for an item that is about to be removed from the collection view. @param sectionController The section controller to perform the transition on. @param index The index of the item being deleted. */ - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; @end ================================================ FILE: Source/IGListKit/IGListUpdatingDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListTransitionData; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN /** A completion block to execute when updates are finished. @param finished Specifies whether or not the update finished. */ NS_SWIFT_NAME(ListUpdatingCompletion) typedef void (^IGListUpdatingCompletion)(BOOL finished); /** A block to be called when the adapter applies changes to the collection view. @param toObjects The new objects in the collection. */ NS_SWIFT_NAME(ListObjectTransitionBlock) typedef void (^IGListObjectTransitionBlock)(NSArray *toObjects); /// A block that contains all of the updates. NS_SWIFT_NAME(ListItemUpdateBlock) typedef void (^IGListItemUpdateBlock)(void); /// A block to be called when an adapter reloads the collection view. NS_SWIFT_NAME(ListReloadUpdateBlock) typedef void (^IGListReloadUpdateBlock)(void); /// A block that returns an array of objects to transition to. NS_SWIFT_NAME(ListToObjectBlock) typedef NSArray * _Nullable (^IGListToObjectBlock)(void); /// A block that returns a collection view to perform updates on. NS_SWIFT_NAME(ListCollectionViewBlock) typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void); /// A block that applies a `UICollectionView` dataSource change NS_SWIFT_NAME(ListDataSourceChangeBlock) typedef void (^IGListDataSourceChangeBlock)(void); /// A block that returns the `IGListTransitionData` needed before an update. NS_SWIFT_NAME(ListTransitionDataBlock) typedef IGListTransitionData * _Nullable (^IGListTransitionDataBlock)(void); /** A block to be called when the adapter applies changes to the collection view. @param data The new data that contains the from/to objects. */ NS_SWIFT_NAME(ListTransitionDataApplyBlock) typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *data); /** Implement this protocol in order to handle both section and row based update events. Implementation should forward or coalesce these events to a backing store or collection. */ NS_SWIFT_NAME(ListUpdatingDelegate) @protocol IGListUpdatingDelegate /** Asks the delegate for the pointer functions for looking up an object in a collection. @return Pointer functions for looking up an object in a collection. @note Since the updating delegate is responsible for transitioning between object sets, it becomes the "source of truth" for how objects and their corresponding section controllers are mapped. This allows the updater to control if objects are looked up by pointer, or more traditionally, with `-hash`/`-isEqual`. For behavior similar to `NSDictionary`, simply return `+[NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality]`. */ - (NSPointerFunctions *)objectLookupPointerFunctions; /** Perform a **section** update from an old array of objects to a new one. @param collectionViewBlock A block returning the collecion view to perform updates on. @param animated A flag indicating if the transition should be animated. @param sectionDataBlock A block that returns the section information (ex: from and to objects) @param applySectionDataBlock A block that must be called when the adapter applies changes to the collection view. @param completion A completion block to execute when the update is finished. @note Implementations determine how to transition between objects. You can perform a diff on the objects, reload each section, or simply call `-reloadData` on the collection view. In the end, the collection view must be setup with a section for each object in the `toObjects` array. The `applySectionDataBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects` that the updater is applying. */ - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(nullable IGListUpdatingCompletion)completion; /** Perform an **item** update block in the collection view. @param collectionViewBlock A block returning the collecion view to perform updates on. @param animated A flag indicating if the transition should be animated. @param itemUpdates A block containing all of the updates. @param completion A completion block to execute when the update is finished. */ - (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock animated:(BOOL)animated itemUpdates:(IGListItemUpdateBlock)itemUpdates completion:(nullable IGListUpdatingCompletion)completion; /** Perform a `[UICollectionView setDataSource:...]` swap within this block. It gives the updater the chance to cancel or execute any on-going updates. The block should be executed synchronously. @param block The block that will actuallty change the `dataSource` */ - (void)performDataSourceChange:(IGListDataSourceChangeBlock)block; /** Completely reload data in the collection. @param collectionViewBlock A block returning the collecion view to reload. @param reloadUpdateBlock A block that must be called when the adapter reloads the collection view. @param completion A completion block to execute when the reload is finished. */ - (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(nullable IGListUpdatingCompletion)completion; /** Tells the delegate to perform item inserts at the given index paths. @param collectionView The collection view on which to perform the transition. @param indexPaths The index paths to insert items into. */ - (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths; /** Tells the delegate to perform item deletes at the given index paths. @param collectionView The collection view on which to perform the transition. @param indexPaths The index paths to delete items from. */ - (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths; /** Tells the delegate to move an item from and to given index paths. @param collectionView The collection view on which to perform the transition. @param fromIndexPath The source index path of the item to move. @param toIndexPath The destination index path of the item to move. */ - (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; /** Tells the delegate to reload an item from and to given index paths. @param collectionView The collection view on which to perform the transition. @param fromIndexPath The source index path of the item to reload. @param toIndexPath The destination index path of the item to reload. @note Since UICollectionView is unable to handle calling -[UICollectionView reloadItemsAtIndexPaths:] safely while also executing insert and delete operations in the same batch updates, the updater must know about the origin and destination of the reload to perform a safe transition. */ - (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; /** Tells the delegate to move a section from and to given indexes. @param collectionView The collection view on which to perform the transition. @param fromIndex The source index of the section to move. @param toIndex The destination index of the section to move. */ - (void)moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex; /** Completely reload each section in the collection view. @param collectionView The collection view to reload. @param sections The sections to reload. */ - (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections; /** True if the updater is currently updating the source of truth, like executing applySectionDataBlock and itemUpdates just before updating the UICollectionView. */ - (BOOL)isInDataUpdateBlock; /** Called when the updater detects an imminent crash, such as when a section controller returns a nil cell. This provides an opportunity to log diagnostic information before the crash occurs. @param collectionView The collection view involved in the crash. @param sectionControllerClass The class of the section controller that caused the issue, if available. */ - (void)willCrashWithCollectionView:(UICollectionView *)collectionView sectionControllerClass:(nullable Class)sectionControllerClass; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/IGListWorkingRangeDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; @class IGListSectionController; NS_ASSUME_NONNULL_BEGIN /** Implement this protocol to receive working range events for a list. The working range is a range *near* the viewport in which you can begin preparing content for display. For example, you could begin decoding images, or warming text caches. */ NS_SWIFT_UI_ACTOR NS_SWIFT_NAME(ListWorkingRangeDelegate) @protocol IGListWorkingRangeDelegate /** Notifies the delegate that an section controller will enter the working range. @param listAdapter The adapter controlling the list. @param sectionController The section controller entering the range. */ - (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController *)sectionController; /** Notifies the delegate that an section controller exited the working range. @param listAdapter The adapter controlling the list. @param sectionController The section controller that exited the range. */ - (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController *)sectionController; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListAdapter+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapter.h" @interface IGListAdapter (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapter+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapter+DebugDescription.h" #import "IGListAdapterInternal.h" #import "IGListAdapterUpdater+DebugDescription.h" #import "IGListDebuggingUtilities.h" #import "IGListSectionMap+DebugDescription.h" #import "UICollectionView+DebugDescription.h" @implementation IGListAdapter (DebugDescription) - (NSString *)debugDescription { NSMutableArray *lines = [NSMutableArray arrayWithObject:[NSString stringWithFormat:@"IGListAdapter %p:", self]]; [lines addObjectsFromArray:IGListDebugIndentedLines([self debugDescriptionLines])]; return [lines componentsJoinedByString:@"\n"]; } - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [debug addObject:[NSString stringWithFormat:@"Updater type: %@", NSStringFromClass(self.updater.class)]]; [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; [debug addObject:[NSString stringWithFormat:@"Collection view delegate: %@", self.collectionViewDelegate]]; [debug addObject:[NSString stringWithFormat:@"Scroll view delegate: %@", self.scrollViewDelegate]]; [debug addObject:[NSString stringWithFormat:@"Is in update block: %@", IGListDebugBOOL(self.isInDataUpdateBlock)]]; [debug addObject:[NSString stringWithFormat:@"View controller: %@", self.viewController]]; if (@available(iOS 10.0, tvOS 10, *)) { [debug addObject:[NSString stringWithFormat:@"Is prefetching enabled: %@", IGListDebugBOOL(self.collectionView.isPrefetchingEnabled)]]; } if (self.registeredCellIdentifiers.count > 0) { [debug addObject:@"Registered cell identifiers:"]; [debug addObject:[self.registeredCellIdentifiers description]]; } if (self.registeredNibNames.count > 0) { [debug addObject:@"Registered nib names:"]; [debug addObject:[self.registeredNibNames description]]; } if (self.registeredSupplementaryViewIdentifiers.count > 0) { [debug addObject:@"Registered supplementary view identifiers:"]; [debug addObject:[self.registeredSupplementaryViewIdentifiers description]]; } if (self.registeredSupplementaryViewNibNames.count > 0) { [debug addObject:@"Registered supplementary view nib names:"]; [debug addObject:self.registeredSupplementaryViewNibNames]; } if ([self.updater isKindOfClass:[IGListAdapterUpdater class]]) { [debug addObject:[NSString stringWithFormat:@"IGListAdapterUpdater instance %p:", self.updater]]; [debug addObjectsFromArray:IGListDebugIndentedLines([(IGListAdapterUpdater *)self.updater debugDescriptionLines])]; } [debug addObject:@"Section map details:"]; [debug addObjectsFromArray:IGListDebugIndentedLines([self.sectionMap debugDescriptionLines])]; if (self.previousSectionMap != nil) { [debug addObject:@"Previous section map details:"]; [debug addObjectsFromArray:IGListDebugIndentedLines([self.previousSectionMap debugDescriptionLines])]; } [debug addObject:@"Collection view details:"]; [debug addObjectsFromArray:IGListDebugIndentedLines([self.collectionView debugDescriptionLines])]; #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapter+UICollectionView.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListAdapter.h" #import "IGListCollectionViewDelegateLayout.h" @interface IGListAdapter (UICollectionView) < UICollectionViewDataSource, IGListCollectionViewDelegateLayout > @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapter+UICollectionView.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapter+UICollectionView.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListAdapterInternal.h" #import "IGListSectionController.h" #import "IGListSectionControllerInternal.h" #import "IGListUpdatingDelegate.h" #import "IGListAdapterInternal.h" @implementation IGListAdapter (UICollectionView) #pragma mark - UICollectionViewDataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { _assertNotInMiddleOfObjectUpdate(self.isInObjectUpdateTransaction); return self.sectionMap.objects.count; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { _assertNotInMiddleOfObjectUpdate(self.isInObjectUpdateTransaction); IGListSectionController * sectionController = [self sectionControllerForSection:section]; IGAssert(sectionController != nil, @"Nil section controller for section %li for item %@. Check your -diffIdentifier and -isEqual: implementations.", (long)section, [self.sectionMap objectForSection:section]); const NSInteger numberOfItems = [sectionController numberOfItems]; IGAssert(numberOfItems >= 0, @"Cannot return negative number of items %li for section controller %@.", (long)numberOfItems, sectionController); return numberOfItems; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { id performanceDelegate = self.performanceDelegate; [performanceDelegate listAdapterWillCallDequeueCell:self]; IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; IGAssert(sectionController != nil, @"Section controller is nil %@", [self _debugDetailsForIndexPath:indexPath]); IGAssert(sectionController.collectionContext != nil, @"sectionController.collectionContext is nil %@", [self _debugDetailsForIndexPath:indexPath]); #if IG_ASSERTIONS_ENABLED if (!_dequeuedCells) { _dequeuedCells = [NSMutableSet new]; } #endif // flag that a cell is being dequeued in case it tries to access a cell in the process _isDequeuingCell = YES; UICollectionViewCell *cell = [sectionController cellForItemAtIndex:indexPath.item]; _isDequeuingCell = NO; IGAssert(cell != nil, @"Returned a nil cell %@", [self _debugDetailsForIndexPath:indexPath]); if (cell) { IGAssert(cell.reuseIdentifier != nil, @"Returned a cell without a reuseIdentifier %@", [self _debugDetailsForIndexPath:indexPath]); if (_dequeuedCells) { // This will cause a crash in iOS 18 IGAssert([_dequeuedCells containsObject:cell], @"Returned a cell (%@) that was not dequeued %@", cell.class, [self _debugDetailsForIndexPath:indexPath]); } } if (cell == nil) { [self.updater willCrashWithCollectionView:collectionView sectionControllerClass:sectionController.class]; } [_dequeuedCells removeAllObjects]; // associate the section controller with the cell so that we know which section controller is using it [self mapView:cell toSectionController:sectionController]; [performanceDelegate listAdapter:self didCallDequeueCell:cell onSectionController:sectionController atIndex:indexPath.item]; return cell; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; id supplementarySource = [sectionController supplementaryViewSource]; #if IG_ASSERTIONS_ENABLED if (!_dequeuedSupplementaryViews) { _dequeuedSupplementaryViews = [NSMutableSet new]; } #endif // flag that a supplementary view is being dequeued in case it tries to access a supplementary view in the process _isDequeuingSupplementaryView = YES; UICollectionReusableView *view = [supplementarySource viewForSupplementaryElementOfKind:kind atIndex:indexPath.item]; _isDequeuingSupplementaryView = NO; IGAssert(view != nil, @"Returned a nil supplementary-view from source %@ %@", supplementarySource.class, [self _debugDetailsForIndexPath:indexPath]); if (view && _dequeuedSupplementaryViews) { // This will cause a crash in iOS 18 IGAssert([_dequeuedSupplementaryViews containsObject:view], @"Returned a supplementary-view (%@) that was not dequeued %@", view.class, [self _debugDetailsForIndexPath:indexPath]); } [_dequeuedSupplementaryViews removeAllObjects]; // associate the section controller with the cell so that we know which section controller is using it [self mapView:view toSectionController:sectionController]; return view; } - (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { const NSInteger sectionIndex = indexPath.section; const NSInteger itemIndex = indexPath.item; IGListSectionController *sectionController = [self sectionControllerForSection:sectionIndex]; return [sectionController canMoveItemAtIndex:itemIndex]; } - (void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { const NSInteger sourceSectionIndex = sourceIndexPath.section; const NSInteger destinationSectionIndex = destinationIndexPath.section; const NSInteger sourceItemIndex = sourceIndexPath.item; const NSInteger destinationItemIndex = destinationIndexPath.item; IGListSectionController *sourceSectionController = [self sectionControllerForSection:sourceSectionIndex]; IGListSectionController *destinationSectionController = [self sectionControllerForSection:destinationSectionIndex]; // this is a move within a section if (sourceSectionController == destinationSectionController) { if ([sourceSectionController canMoveItemAtIndex:sourceItemIndex toIndex:destinationItemIndex]) { [self moveInSectionControllerInteractive:sourceSectionController fromIndex:sourceItemIndex toIndex:destinationItemIndex]; } else { // otherwise this is a move of an _item_ from one section to another section // we need to revert the change as it's too late to cancel [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } return; } // this is a reordering of sections themselves if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1) { // perform view changes in the collection view [self moveSectionControllerInteractive:sourceSectionController fromIndex:sourceSectionIndex toIndex:destinationSectionIndex]; return; } // otherwise this is a move of an _item_ from one section to another section // this is not currently supported, so we need to revert the change as it's too late to cancel [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; } #pragma mark - UICollectionViewDelegate - (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath { IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; return [sectionController shouldSelectItemAtIndex:indexPath.item]; } - (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath { IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; return [sectionController shouldDeselectItemAtIndex:indexPath.item]; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didSelectItemAtIndexPath:indexPath]; } IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; [sectionController didSelectItemAtIndex:indexPath.item]; if (self.autoDeselectEnabled) { // We go directly to the collection-view since we already have the full NSIndexPath [self.collectionView deselectItemAtIndexPath:indexPath animated:YES]; } } - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didDeselectItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didDeselectItemAtIndexPath:indexPath]; } IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; [sectionController didDeselectItemAtIndex:indexPath.item]; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { id performanceDelegate = self.performanceDelegate; [performanceDelegate listAdapterWillCallDisplayCell:self]; // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; } IGListSectionController *sectionController = [self sectionControllerForView:cell]; // if the section controller relationship was destroyed, reconnect it // this happens with iOS 10 UICollectionView display range changes if (sectionController == nil) { sectionController = [self sectionControllerForSection:indexPath.section]; [self mapView:cell toSectionController:sectionController]; } id object = [self.sectionMap objectForSection:indexPath.section]; [self.displayHandler willDisplayCell:cell forListAdapter:self sectionController:sectionController object:object indexPath:indexPath]; _isSendingWorkingRangeDisplayUpdates = YES; [self.workingRangeHandler willDisplayItemAtIndexPath:indexPath forListAdapter:self]; _isSendingWorkingRangeDisplayUpdates = NO; [performanceDelegate listAdapter:self didCallDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { id performanceDelegate = self.performanceDelegate; [performanceDelegate listAdapterWillCallEndDisplayCell:self]; // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; } IGListSectionController *const sectionController = [self sectionControllerForView:cell]; [self.displayHandler didEndDisplayingCell:cell forListAdapter:self sectionController:sectionController indexPath:indexPath]; [self.workingRangeHandler didEndDisplayingItemAtIndexPath:indexPath forListAdapter:self]; // break the association between the cell and the section controller [self removeMapForView:cell]; [performanceDelegate listAdapter:self didCallEndDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; } - (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]) { [collectionViewDelegate collectionView:collectionView willDisplaySupplementaryView:view forElementKind:elementKind atIndexPath:indexPath]; } IGListSectionController *sectionController = [self sectionControllerForView:view]; // if the section controller relationship was destroyed, reconnect it // this happens with iOS 10 UICollectionView display range changes if (sectionController == nil) { sectionController = [self sectionControllerForSection:indexPath.section]; [self mapView:view toSectionController:sectionController]; } id object = [self.sectionMap objectForSection:indexPath.section]; [self.displayHandler willDisplaySupplementaryView:view forListAdapter:self sectionController:sectionController object:object indexPath:indexPath]; } - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didEndDisplayingSupplementaryView:view forElementOfKind:elementKind atIndexPath:indexPath]; } IGListSectionController *sectionController = [self sectionControllerForView:view]; [self.displayHandler didEndDisplayingSupplementaryView:view forListAdapter:self sectionController:sectionController indexPath:indexPath]; [self removeMapForView:view]; } - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath { // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didHighlightItemAtIndexPath:indexPath]; } IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; [sectionController didHighlightItemAtIndex:indexPath.item]; } - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath { // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)]) { [collectionViewDelegate collectionView:collectionView didUnhighlightItemAtIndexPath:indexPath]; } IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; [sectionController didUnhighlightItemAtIndex:indexPath.item]; } - (NSIndexPath *)indexPathForPreferredFocusedViewInCollectionView:(UICollectionView *)collectionView { // In case the delegate responds, it should take priority id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(indexPathForPreferredFocusedViewInCollectionView:)]) { return [collectionViewDelegate indexPathForPreferredFocusedViewInCollectionView:collectionView]; } if (IGListExperimentEnabled(self.experiments, IGListExperimentFixPreferredFocusedView)) { // The default implementation of `-[UICollectionView preferredFocusedView]` can create/dequeue off-screen // cells, which causes perf issues and bugs. return [[collectionView indexPathsForVisibleItems] firstObject]; } return nil; } #if !TARGET_OS_TV #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point API_AVAILABLE(ios(13.0)) { // forward this method to the delegate b/c this implementation will steal the message from the proxy id collectionViewDelegate = self.collectionViewDelegate; if ([collectionViewDelegate respondsToSelector:@selector(collectionView:contextMenuConfigurationForItemAtIndexPath:point:)]) { return [collectionViewDelegate collectionView:collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:point]; } IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; return [sectionController contextMenuConfigurationForItemAtIndex:indexPath.item point:point]; } #pragma clang diagnostic pop #endif #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); CGSize size = [self sizeForItemAtIndexPath:indexPath]; IGAssert(!isnan(size.height), @"IGListAdapter returned NaN height = %f %@", size.height, [self _debugDetailsForIndexPath:indexPath]); IGAssert(!isnan(size.width), @"IGListAdapter returned NaN width = %f %@", size.width, [self _debugDetailsForIndexPath:indexPath]); return size; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); return [[self sectionControllerForSection:section] inset]; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); return [[self sectionControllerForSection:section] minimumLineSpacing]; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); return [[self sectionControllerForSection:section] minimumInteritemSpacing]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { IGWarn(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:indexPath]; } #pragma mark - IGListCollectionViewDelegateLayout - (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath { IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; if (sectionController.transitionDelegate) { return [sectionController.transitionDelegate listAdapter:self customizedInitialLayoutAttributes:attributes sectionController:sectionController atIndex:indexPath.item]; } return attributes; } - (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath { IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; if (sectionController.transitionDelegate) { return [sectionController.transitionDelegate listAdapter:self customizedFinalLayoutAttributes:attributes sectionController:sectionController atIndex:indexPath.item]; } return attributes; } #pragma mark - Assert helpers - (NSString *)_debugDetailsForIndexPath:(NSIndexPath *)indexPath __attribute__((objc_direct)) { NSObject *const object = [self objectAtSection:indexPath.section]; IGListSectionController *const sectionController = [self sectionControllerForSection:indexPath.section]; return [NSString stringWithFormat:@"{indexPath: %lu-%lu, object: %@, sectionController: %@, dataSource: %@}", indexPath.section, indexPath.item, object.class, sectionController.class, self.dataSource.class]; } static void _assertNotInMiddleOfObjectUpdate(BOOL isInObjectUpdateTransaction) { IGAssert(isInObjectUpdateTransaction == NO, @"The UICollectionView is attempting to update its data while the IGListAdapter is in a middle of updating the object list. This will cause inconsistencies and potentially crashes (which are hard to debug). The most common cause is a section controller tries to access/modify the UICollectionView in -didUpdateToObject."); } @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapterDelegateAnnouncerInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterDelegateAnnouncer.h" NS_ASSUME_NONNULL_BEGIN @interface IGListAdapterDelegateAnnouncer () - (void)announceObjectDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index; - (void)announceObjectEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object index:(NSInteger)index; - (void)announceCellDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object cell:(UICollectionViewCell *)cell indexPath:(NSIndexPath *)indexPath; - (void)announceCellEndDisplayWithAdapter:(IGListAdapter *)listAdapter object:(id)object cell:(UICollectionViewCell *)cell indexPath:(NSIndexPath *)indexPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListAdapterInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapter.h" #import "IGListBatchContext.h" #import "IGListCollectionContext.h" #import "IGListAdapter+UICollectionView.h" #import "IGListAdapterProxy.h" #import "IGListDisplayHandler.h" #import "IGListSectionMap.h" #import "IGListWorkingRangeHandler.h" NS_ASSUME_NONNULL_BEGIN /// Generate a string representation of a reusable view class when registering with a UICollectionView. NS_INLINE NSString *IGListReusableViewIdentifier(Class viewClass, NSString * _Nullable kind, NSString * _Nullable givenReuseIdentifier) { return [NSString stringWithFormat:@"%@%@%@", kind ?: @"", givenReuseIdentifier ?: @"", NSStringFromClass(viewClass)]; } @interface IGListAdapter () < IGListCollectionContext, IGListBatchContext > { __weak UICollectionView *_collectionView; BOOL _isDequeuingCell; NSMutableSet *_dequeuedCells; BOOL _isDequeuingSupplementaryView; NSMutableSet *_dequeuedSupplementaryViews; BOOL _isSendingWorkingRangeDisplayUpdates; } @property (nonatomic, strong) id updater; @property (nonatomic, strong, readonly) IGListSectionMap *sectionMap; @property (nonatomic, strong, readonly) IGListDisplayHandler *displayHandler; @property (nonatomic, strong, readonly) IGListWorkingRangeHandler *workingRangeHandler; @property (nonatomic, strong, nullable) IGListAdapterProxy *delegateProxy; // Set as a property for unit testing @property (nonatomic, strong, nullable) IGListAdapterDelegateAnnouncer *globalDelegateAnnouncer; @property (nonatomic, strong, nullable) UIView *emptyBackgroundView; // We need to special case interactive section moves that are moved to the last position @property (nonatomic) BOOL isLastInteractiveMoveToLastSectionIndex; // We're in the middle of updating the objects. @property (nonatomic) BOOL isInObjectUpdateTransaction; /** When making object updates inside a batch update block, delete operations must use the section /before/ any moves take place. This includes when other objects are deleted or inserted ahead of the section controller making the mutations. In order to account for this we must track when the adapter is in the middle of an update block as well as the section controller mapping prior to the transition. Note that the previous section controller map is destroyed as soon as a transition is finished so there is no dangling objects or section controllers. During this period, we're updating IGListKit's internal data, but not the UICollectionView just yet. This is a dangerous time, since the internal data might only be partially updated. */ @property (nonatomic, assign, readonly) BOOL isInDataUpdateBlock; @property (nonatomic, strong, nullable) IGListSectionMap *previousSectionMap; /** Set of cell identifiers registered with the list context. Identifiers are constructed with the `IGListReusableViewIdentifier` function. */ @property (nonatomic, strong) NSMutableSet *registeredCellIdentifiers; @property (nonatomic, strong) NSMutableSet *registeredNibNames; @property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewIdentifiers; @property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewNibNames; - (void)mapView:(__kindof UIView *)view toSectionController:(IGListSectionController *)sectionController; - (nullable IGListSectionController *)sectionControllerForView:(__kindof UIView *)view; - (void)removeMapForView:(__kindof UIView *)view; - (NSArray *)indexPathsFromSectionController:(IGListSectionController *)sectionController indexes:(NSIndexSet *)indexes usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock; - (nullable NSIndexPath *)indexPathForSectionController:(IGListSectionController *)controller index:(NSInteger)index usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListAdapterProxy.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif @class IGListAdapter; NS_ASSUME_NONNULL_BEGIN /** A proxy that sends a custom set of selectors to an IGListAdapter object and the rest to a UICollectionViewDelegate target. */ IGLK_SUBCLASSING_RESTRICTED @interface IGListAdapterProxy : NSProxy /** Create a new proxy object with targets and interceptor. @param collectionViewTarget A UICollectionViewDelegate conforming object that receives non-intercepted messages. @param scrollViewTarget A UIScrollViewDelegate conforming object that receives non-intercepted messages. @param interceptor An IGListAdapter object that intercepts a set of messages. @return A new IGListAdapterProxy object. */ - (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget scrollViewTarget:(nullable id)scrollViewTarget interceptor:(IGListAdapter *)interceptor; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListAdapterProxy.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterProxy.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListCollectionViewDelegateLayout.h" /** Define messages that you want the IGListAdapter object to intercept. Pattern copied from https://github.com/facebook/AsyncDisplayKit/blob/7b112a2dcd0391ddf3671f9dcb63521f554b78bd/AsyncDisplayKit/ASCollectionView.mm#L34-L53 */ static BOOL isInterceptedSelector(SEL sel) { return ( // UIScrollViewDelegate sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewWillBeginDragging:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) || sel == @selector(scrollViewDidEndDecelerating:) || // UICollectionViewDelegate sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || sel == @selector(collectionView:shouldSelectItemAtIndexPath:) || sel == @selector(collectionView:didSelectItemAtIndexPath:) || sel == @selector(collectionView:shouldDeselectItemAtIndexPath:) || sel == @selector(collectionView:didDeselectItemAtIndexPath:) || sel == @selector(collectionView:didHighlightItemAtIndexPath:) || sel == @selector(collectionView:didUnhighlightItemAtIndexPath:) || sel == @selector(indexPathForPreferredFocusedViewInCollectionView:) || sel == @selector(collectionView:contextMenuConfigurationForItemAtIndexPath:point:) || // UICollectionViewDelegateFlowLayout sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || sel == @selector(collectionView:layout:insetForSectionAtIndex:) || sel == @selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:) || sel == @selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:) || sel == @selector(collectionView:layout:referenceSizeForFooterInSection:) || sel == @selector(collectionView:layout:referenceSizeForHeaderInSection:) || // IGListCollectionViewDelegateLayout sel == @selector(collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:) || sel == @selector(collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:) ); } @interface IGListAdapterProxy () { __weak id _collectionViewTarget; __weak id _scrollViewTarget; __weak IGListAdapter *_interceptor; } @end @implementation IGListAdapterProxy - (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget scrollViewTarget:(nullable id)scrollViewTarget interceptor:(IGListAdapter *)interceptor { IGParameterAssert(interceptor != nil); // -[NSProxy init] is undefined if (self) { _collectionViewTarget = collectionViewTarget; _scrollViewTarget = scrollViewTarget; _interceptor = interceptor; } return self; } - (BOOL)respondsToSelector:(SEL)aSelector { return isInterceptedSelector(aSelector) || [_collectionViewTarget respondsToSelector:aSelector] || [_scrollViewTarget respondsToSelector:aSelector]; } - (id)forwardingTargetForSelector:(SEL)aSelector { if (isInterceptedSelector(aSelector)) { return _interceptor; } // since UICollectionViewDelegate is a superset of UIScrollViewDelegate, first check if the method exists in // _scrollViewTarget, otherwise use the _collectionViewTarget return [_scrollViewTarget respondsToSelector:aSelector] ? _scrollViewTarget : _collectionViewTarget; } // handling unimplemented methods and nil target/interceptor // https://github.com/Flipboard/FLAnimatedImage/blob/76a31aefc645cc09463a62d42c02954a30434d7d/FLAnimatedImage/FLAnimatedImage.m#L786-L807 - (void)forwardInvocation:(NSInvocation *)invocation { void *nullPointer = NULL; [invocation setReturnValue:&nullPointer]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterUpdater.h" @interface IGListAdapterUpdater (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterUpdater+DebugDescription.h" #import "IGListAdapterUpdaterInternal.h" #import "IGListBatchUpdateData+DebugDescription.h" #import "IGListDebuggingUtilities.h" #import "IGListUpdateTransactable.h" @implementation IGListAdapterUpdater (DebugDescription) - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [debug addObject:@"Options:"]; NSArray *options = @[ [NSString stringWithFormat:@"sectionMovesAsDeletesInserts: %@", IGListDebugBOOL(self.sectionMovesAsDeletesInserts)], [NSString stringWithFormat:@"singleItemSectionUpdates: %@", IGListDebugBOOL(self.singleItemSectionUpdates)], [NSString stringWithFormat:@"preferItemReloadsForSectionReloads: %@", IGListDebugBOOL(self.preferItemReloadsForSectionReloads)], [NSString stringWithFormat:@"allowsReloadingOnTooManyUpdates: %@", IGListDebugBOOL(self.allowsReloadingOnTooManyUpdates)], [NSString stringWithFormat:@"allowsBackgroundDiffing: %@", IGListDebugBOOL(self.allowsBackgroundDiffing)] ]; [debug addObjectsFromArray:IGListDebugIndentedLines(options)]; const IGListBatchUpdateState state = self.transaction ? [self.transaction state] : IGListBatchUpdateStateIdle; NSString *stateString; switch (state) { case IGListBatchUpdateStateIdle: stateString = @"Idle"; break; case IGListBatchUpdateStateQueuedBatchUpdate: stateString = @"Queued batch update"; break; case IGListBatchUpdateStateExecutedBatchUpdateBlock: stateString = @"Executed batch update block"; break; case IGListBatchUpdateStateExecutingBatchUpdateBlock: stateString = @"Executing batch update block"; break; default: /* unknown */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(IGListBatchUpdateState, state); } [debug addObject:[NSString stringWithFormat:@"State: %@", stateString]]; #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListBatchUpdateData; @class IGListIndexSetResult; @class IGListReloadIndexPath; @class IGListMoveIndexPath; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN void IGListConvertReloadToDeleteInsert(NSMutableIndexSet *reloads, NSMutableIndexSet *deletes, NSMutableIndexSet *inserts, IGListIndexSetResult *result, NSArray> *fromObjects); IGListBatchUpdateData *IGListApplyUpdatesToCollectionView(UICollectionView *collectionView, IGListIndexSetResult *diffResult, NSMutableIndexSet *sectionReloads, NSMutableArray *itemInserts, NSMutableArray *itemDeletes, NSMutableArray *itemReloads, NSMutableArray *itemMoves, NSArray> *fromObjects, BOOL sectionMovesAsDeletesInserts, BOOL preferItemReloadsForSectionReloads); NSIndexSet *IGListSectionIndexFromIndexPaths(NSArray *indexPaths); NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListAdapterUpdaterHelpers.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterUpdaterHelpers.h" #if !__has_include() #import "IGListAssert.h" #import "IGListBatchUpdateData.h" #import "IGListDiffable.h" #import "IGListIndexSetResult.h" #else #import #import #import #import #endif #import "IGListReloadIndexPath.h" #import "UICollectionView+IGListBatchUpdateData.h" void IGListConvertReloadToDeleteInsert(NSMutableIndexSet *reloads, NSMutableIndexSet *deletes, NSMutableIndexSet *inserts, IGListIndexSetResult *result, NSArray> *fromObjects) { // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts const BOOL hasObjects = [fromObjects count] > 0; [[reloads copy] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { // if a diff was not performed, there are no changes. instead use the same index that was originally queued id diffIdentifier = hasObjects ? [fromObjects[idx] diffIdentifier] : nil; const NSInteger from = hasObjects ? [result oldIndexForIdentifier:diffIdentifier] : idx; const NSInteger to = hasObjects ? [result newIndexForIdentifier:diffIdentifier] : idx; [reloads removeIndex:from]; // if a reload is queued outside the diff and the object was inserted or deleted it cannot be if (from != NSNotFound && to != NSNotFound) { [deletes addIndex:from]; [inserts addIndex:to]; } else { IGAssert([result.deletes containsIndex:idx], @"Reloaded section %lu was not found in deletes with from: %li, to: %li, deletes: %@, fromClass: %@", (unsigned long)idx, (long)from, (long)to, deletes, [(id)fromObjects[idx] class]); } }]; } static NSArray *convertSectionReloadToItemUpdates(NSIndexSet *sectionReloads, UICollectionView *collectionView) { NSMutableArray *updates = [NSMutableArray new]; [sectionReloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { NSUInteger numberOfItems = [collectionView numberOfItemsInSection:sectionIndex]; for (NSUInteger itemIndex = 0; itemIndex < numberOfItems; itemIndex++) { [updates addObject:[NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex]]; } }]; return [updates copy]; } IGListBatchUpdateData *IGListApplyUpdatesToCollectionView(UICollectionView *collectionView, IGListIndexSetResult *diffResult, NSMutableIndexSet *sectionReloads, NSMutableArray *itemInserts, NSMutableArray *itemDeletes, NSMutableArray *itemReloads, NSMutableArray *itemMoves, NSArray> *fromObjects, BOOL sectionMovesAsDeletesInserts, BOOL preferItemReloadsForSectionReloads) { NSSet *moves = [[NSSet alloc] initWithArray:diffResult.moves]; // combine section reloads from the diff and manual reloads via reloadItems: NSMutableIndexSet *reloads = [diffResult.updates mutableCopy]; [reloads addIndexes:sectionReloads]; NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy]; NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy]; NSMutableArray *itemUpdates = [NSMutableArray new]; if (sectionMovesAsDeletesInserts) { for (IGListMoveIndex *move in moves) { [deletes addIndex:move.from]; [inserts addIndex:move.to]; } // clear out all moves moves = [NSSet new]; } // Item reloads are not safe, if any section moves happened or there are inserts/deletes. if (preferItemReloadsForSectionReloads && moves.count == 0 && inserts.count == 0 && deletes.count == 0 && reloads.count > 0) { [reloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { NSMutableIndexSet *localIndexSet = [NSMutableIndexSet indexSetWithIndex:sectionIndex]; if ((NSInteger)sectionIndex < [collectionView numberOfSections] && (NSInteger)sectionIndex < [collectionView.dataSource numberOfSectionsInCollectionView:collectionView] && [collectionView numberOfItemsInSection:(NSInteger)sectionIndex] == [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:sectionIndex]) { // Perfer to do item reloads instead, if the number of items in section is unchanged. [itemUpdates addObjectsFromArray:convertSectionReloadToItemUpdates(localIndexSet, collectionView)]; } else { // Otherwise, fallback to convert into delete+insert section operation. IGListConvertReloadToDeleteInsert(localIndexSet, deletes, inserts, diffResult, fromObjects); } }]; } else { // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects); } NSSet *uniqueDeletes = [NSSet setWithArray:itemDeletes]; NSMutableSet *reloadDeletePaths = [NSMutableSet new]; NSMutableSet *reloadInsertPaths = [NSMutableSet new]; for (IGListReloadIndexPath *reload in itemReloads) { if (![uniqueDeletes containsObject:reload.fromIndexPath]) { [reloadDeletePaths addObject:reload.fromIndexPath]; [reloadInsertPaths addObject:reload.toIndexPath]; } } [itemDeletes addObjectsFromArray:[reloadDeletePaths allObjects]]; [itemInserts addObjectsFromArray:[reloadInsertPaths allObjects]]; IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts deleteSections:deletes moveSections:moves insertIndexPaths:itemInserts deleteIndexPaths:itemDeletes updateIndexPaths:itemUpdates moveIndexPaths:itemMoves]; [collectionView ig_applyBatchUpdateData:updateData]; return updateData; } NSIndexSet *IGListSectionIndexFromIndexPaths(NSArray *indexPaths) { NSMutableIndexSet *sections = [NSMutableIndexSet new]; for (NSIndexPath *indexPath in indexPaths) { [sections addIndex:(NSUInteger)indexPath.section]; } return sections; } ================================================ FILE: Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListAdapterUpdater.h" #import "IGListBatchUpdateState.h" @class IGListUpdateTransactionBuilder; @protocol IGListUpdateTransactable; NS_ASSUME_NONNULL_BEGIN @interface IGListAdapterUpdater () - (BOOL)hasChanges; /// Force an update to start - (void)update; - (id)transaction; - (IGListUpdateTransactionBuilder *)transactionBuilder; - (IGListUpdateTransactionBuilder *)lastTransactionBuilder; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListArrayUtilsInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @protocol IGListDiffable; /// Returns a copy of the provided array, with all duplicates /// of objects with the same `diffIdentifier` value removed. /// - Parameter objects: The list of diffable objects to filter. NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray> *objects); ================================================ FILE: Source/IGListKit/Internal/IGListArrayUtilsInternal.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListArrayUtilsInternal.h" #if !__has_include() #import "IGListBatchUpdateData.h" #import "IGListDiffable.h" #else #import #import #endif NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray> *objects) { if (objects == nil) { return nil; } NSMapTable *identifierMap = [NSMapTable strongToStrongObjectsMapTable]; NSMutableArray *uniqueObjects = [NSMutableArray new]; for (id object in objects) { id diffIdentifier = [object diffIdentifier]; id previousObject = [identifierMap objectForKey:diffIdentifier]; if (diffIdentifier != nil && previousObject == nil) { [identifierMap setObject:object forKey:diffIdentifier]; [uniqueObjects addObject:object]; } else { IGLKLog(@"Duplicate identifier %@ for object %@ with object %@", diffIdentifier, object, previousObject); } } return uniqueObjects; } ================================================ FILE: Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListBatchUpdateData.h" #else #import #endif @interface IGListBatchUpdateData (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBatchUpdateData+DebugDescription.h" @implementation IGListBatchUpdateData (DebugDescription) - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [debug addObject:[NSString stringWithFormat:@"Insert sections: %@", self.insertSections]]; [debug addObject:[NSString stringWithFormat:@"Delete sections: %@", self.deleteSections]]; for (IGListMoveIndex *move in self.moveSections) { [debug addObject:[NSString stringWithFormat:@"Move from section %li to %li", (long)move.from, (long)move.to]]; } for (NSIndexPath *path in self.deleteIndexPaths) { [debug addObject:[NSString stringWithFormat:@"Delete section %li item %li", (long)path.section, (long)path.item]]; } for (NSIndexPath *path in self.insertIndexPaths) { [debug addObject:[NSString stringWithFormat:@"Insert section %li item %li", (long)path.section, (long)path.item]]; } for (IGListMoveIndexPath *move in self.moveIndexPaths) { [debug addObject:[NSString stringWithFormat:@"Move from section %li item %li to section %li item %li", (long)move.from.section, (long)move.from.item, (long)move.to.section, (long)move.to.item]]; } #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/IGListBatchUpdateState.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import typedef NS_ENUM (NSInteger, IGListBatchUpdateState) { IGListBatchUpdateStateIdle, IGListBatchUpdateStateQueuedBatchUpdate, IGListBatchUpdateStateExecutingBatchUpdateBlock, IGListBatchUpdateStateExecutedBatchUpdateBlock, }; ================================================ FILE: Source/IGListKit/Internal/IGListBatchUpdateTransaction.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListUpdatingDelegate.h" #else #import #endif #import "IGListUpdateTransactable.h" @class IGListAdapterUpdater; @protocol IGListAdapterUpdaterDelegate; NS_ASSUME_NONNULL_BEGIN /// Handles a batch update transaction IGLK_SUBCLASSING_RESTRICTED @interface IGListBatchUpdateTransaction : NSObject - (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock updater:(IGListAdapterUpdater *)updater delegate:(nullable id)delegate config:(IGListUpdateTransactationConfig)config animated:(BOOL)animated sectionDataBlock:(nullable IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(nullable IGListTransitionDataApplyBlock)applySectionDataBlock itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListBatchUpdateTransaction.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBatchUpdateTransaction.h" #if !__has_include() #import "IGListAssert.h" #import "IGListDiffable.h" #import "IGListDiff.h" #else #import #import #import #endif #import "IGListAdapterUpdaterDelegate.h" #import "IGListAdapterUpdaterHelpers.h" #import "IGListIndexSetResultInternal.h" #import "IGListItemUpdatesCollector.h" #import "IGListMoveIndexPathInternal.h" #import "IGListReloadIndexPath.h" #import "IGListTransitionData.h" #import "UICollectionView+IGListBatchUpdateData.h" #import "IGListPerformDiff.h" typedef NS_ENUM (NSInteger, IGListBatchUpdateTransactionMode) { IGListBatchUpdateTransactionModeCancellable, IGListBatchUpdateTransactionModeNotCancellable, IGListBatchUpdateTransactionModeCancelled, }; @interface IGListBatchUpdateTransaction () // Given @property (nonatomic, copy, readonly) UICollectionView *collectionView; @property (nonatomic, weak, readonly) IGListAdapterUpdater *updater; @property (nonatomic, weak, readonly, nullable) id delegate; @property (nonatomic, assign, readonly) IGListUpdateTransactationConfig config; @property (nonatomic, assign, readonly) BOOL animated; @property (nonatomic, copy, readonly, nullable) IGListTransitionData *sectionData; @property (nonatomic, copy, readonly, nullable) IGListTransitionDataApplyBlock applySectionDataBlock; @property (nonatomic, copy, readonly) NSArray *itemUpdateBlocks; @property (nonatomic, copy, readonly) NSArray *completionBlocks; // Internal @property (nonatomic, strong, readonly) IGListItemUpdatesCollector *inUpdateItemCollector; @property (nonatomic, copy, readonly) NSMutableArray *inUpdateCompletionBlocks; @property (nonatomic, assign, readwrite) IGListBatchUpdateState state; @property (nonatomic, assign, readwrite) IGListBatchUpdateTransactionMode mode; @property (nonatomic, strong, readwrite, nullable) IGListBatchUpdateData *actualCollectionViewUpdates; @end @implementation IGListBatchUpdateTransaction - (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock updater:(IGListAdapterUpdater *)updater delegate:(id)delegate config:(IGListUpdateTransactationConfig)config animated:(BOOL)animated sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks { if (self = [super init]) { _collectionView = collectionViewBlock ? collectionViewBlock() : nil; _updater = updater; _delegate = delegate; _config = config; _animated = animated; _sectionData = sectionDataBlock ? sectionDataBlock() : nil; _applySectionDataBlock = [applySectionDataBlock copy]; _itemUpdateBlocks = [itemUpdateBlocks copy]; _completionBlocks = [completionBlocks copy]; _inUpdateItemCollector = [IGListItemUpdatesCollector new]; _state = IGListBatchUpdateStateIdle; _mode = IGListBatchUpdateTransactionModeCancellable; } return self; } #pragma mark - Update - (void)begin { // bail early if the collection view has been deallocated in the time since the update was queued if (self.collectionView == nil) { [self _bail]; return; } #ifdef DEBUG for (id obj in self.sectionData.toObjects) { IGAssert([obj conformsToProtocol:@protocol(IGListDiffable)], @"In order to use IGListAdapterUpdater, object %@ must conform to IGListDiffable", obj); IGAssert([obj diffIdentifier] != nil, @"Cannot have a nil diffIdentifier for object %@", obj); } #endif // disables multiple performBatchUpdates: from happening at the same time self.state = IGListBatchUpdateStateQueuedBatchUpdate; [self _diff]; } - (void)_diff { IGListTransitionData *data = self.sectionData; [self.delegate listAdapterUpdater:self.updater willDiffFromObjects:data.fromObjects toObjects:data.toObjects]; __weak __typeof__(self) weakSelf = self; IGListPerformDiffWithData(data, self.collectionView, self.config.allowsBackgroundDiffing, self.config.adaptiveDiffingExperimentConfig, ^(IGListIndexSetResult * _Nonnull result, BOOL onBackground) { [weakSelf _didDiff:result onBackground:onBackground]; }); } - (void)_didDiff:(IGListIndexSetResult *)diffResult onBackground:(BOOL)onBackground { if (self.mode == IGListBatchUpdateTransactionModeCancelled) { // Cancelling should have already taken care of the completion blocks return; } // After this point, we can assume that the update has began and there's no turning back. self.mode = IGListBatchUpdateTransactionModeNotCancellable; [self.delegate listAdapterUpdater:self.updater didDiffWithResults:diffResult onBackgroundThread:onBackground]; @try { // Keeping a pointer to self.collectionView.dataSource, because it can get deallocated before the UICollectionView and crash id const collectionViewDataSource = self.collectionView.dataSource; if (collectionViewDataSource == nil) { // If the data source is nil, we should not call any collection view update. [self _bail]; } else if (diffResult.changeCount > 100 && self.config.allowsReloadingOnTooManyUpdates) { [self _reload]; } else if (self.sectionData && [self.collectionView numberOfSections] != (NSInteger)self.sectionData.fromObjects.count) { // If data is nil, there are no section updates. IGWarnAssert(@"The UICollectionView's section count (%li) didn't match the IGListAdapter's count (%li), so we can't performBatchUpdates. Falling back to reloadData.", (long)[self.collectionView numberOfSections], (long)self.sectionData.fromObjects.count); [self _reload]; } else { [self _applyDiff:diffResult]; } } @catch (NSException *exception) { [self.delegate listAdapterUpdater:self.updater collectionView:self.collectionView willCrashWithException:exception fromObjects:self.sectionData.fromObjects toObjects:self.sectionData.toObjects diffResult:diffResult updates:(id)_actualCollectionViewUpdates]; @throw exception; } } - (void)_applyDiff:(IGListIndexSetResult *)diffResult { [self.delegate listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:self.sectionData.fromObjects toObjects:self.sectionData.toObjects listIndexSetResult:diffResult animated:self.animated]; void (^updates)(void) = ^ { [self _applyDataUpdates]; [self _applyCollectioViewUpdates:diffResult]; }; void (^completion)(BOOL) = ^(BOOL finished) { [self _didPerformBatchUpdate:finished]; }; @try { if (self.animated) { [self.collectionView performBatchUpdates:updates completion:completion]; } else { [UIView performWithoutAnimation:^{ [self.collectionView performBatchUpdates:updates completion:completion]; }]; } } @catch (NSException *exception) { /// Currently, we don't throw on `NSInternalInconsistencyException`, like the comment below explains. This was a temporary workaround for the large /// volume of exceptions that started with Xcode 14.3. Now, lets use this experiment flag to slowly reintroduce it, and eventually remove the workaround. const BOOL ignoreException = !IGListExperimentEnabled(self.config.experiments, IGListExperimentThrowOnInconsistencyException); if (ignoreException && [[exception name] isEqualToString:NSInternalInconsistencyException]) { /// As part of S342566 we have to recover from crashing the app since Xcode 14.3 has shipped /// with a different build SDK that changes the runtime behavior of -performBatchUpdates: issues. /// When we are performing batch updates, it's on us to advance the data source to the new state /// inside the updates closure. /// The data source must return the old counts up until the updates closure executes, and must return /// the new counts after the updates closure finishes executing. /// In prior iOS releases, UICollectionView would log an error message to the console for certain cases /// of invalid updates, and instead fall back to reloadData. Using reloadData is destructive to UI state /// and can negatively impact performance, but this was kept the app running so far without us noticing! /// Now that UIKit has changed this runtime behavior we are going to apply the same workaround for the crash while we work /// with our product team to properly fix their data source changes outside of the -performBatchUpdatesBlock: /// IGLisKit processed a new being as an assert that requires investigation, /// since it will be processed as invalid data source state that needs a reload. IGFailure(@"IGListKit caught exception (%@): %@", exception.name, exception.reason); [self begin]; return; } else { [self.delegate listAdapterUpdater:self.updater collectionView:self.collectionView willCrashWithException:exception fromObjects:self.sectionData.fromObjects toObjects:self.sectionData.toObjects diffResult:diffResult updates:(id)_actualCollectionViewUpdates]; @throw exception; } } } - (void)_applyDataUpdates { self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; // run the update block so that the adapter can set its items. this makes sure that just before the update is // committed that the data source is updated to the /latest/ "toObjects". this makes the data source in sync // with the items that the updater is transitioning to if (self.applySectionDataBlock != nil && self.sectionData != nil) { self.applySectionDataBlock((IGListTransitionData *)self.sectionData); } // execute each item update block which should make calls like insert, delete, and reload for index paths // we collect all mutations in corresponding sets on self, then filter based on UICollectionView shortcomings // call after the objectTransitionBlock so section level mutations happen before any items for (IGListItemUpdateBlock block in self.itemUpdateBlocks) { block(); } self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; } - (void)_applyCollectioViewUpdates:(IGListIndexSetResult *)diffResult { if (self.config.singleItemSectionUpdates) { [self.collectionView deleteSections:diffResult.deletes]; [self.collectionView insertSections:diffResult.inserts]; for (IGListMoveIndex *move in diffResult.moves) { [self.collectionView moveSection:move.from toSection:move.to]; } // NOTE: for section updates, it's updated in the IGListSectionController's -didUpdateToObject:, since there is *only* 1 cell for the section, we can just update that cell. self.actualCollectionViewUpdates = [[IGListBatchUpdateData alloc] initWithInsertSections:diffResult.inserts deleteSections:diffResult.deletes moveSections:[NSSet setWithArray:diffResult.moves] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; } else { self.actualCollectionViewUpdates = IGListApplyUpdatesToCollectionView(self.collectionView, diffResult, self.inUpdateItemCollector.sectionReloads, self.inUpdateItemCollector.itemInserts, self.inUpdateItemCollector.itemDeletes, self.inUpdateItemCollector.itemReloads, self.inUpdateItemCollector.itemMoves, self.sectionData.fromObjects ?: @[], self.config.sectionMovesAsDeletesInserts, self.config.preferItemReloadsForSectionReloads); } } - (void)_didPerformBatchUpdate:(BOOL)finished { if (self.actualCollectionViewUpdates) { [self.delegate listAdapterUpdater:self.updater didPerformBatchUpdates:(IGListBatchUpdateData *)self.actualCollectionViewUpdates collectionView:self.collectionView]; } [self _executeCompletionAsFinished:finished]; } - (void)_executeCompletionAsFinished:(BOOL)finished { for (IGListUpdatingCompletion block in self.completionBlocks) { block(finished); } // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any // re-entrant updates. NSArray *inUpdateCompletionBlocks = [_inUpdateCompletionBlocks copy]; for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { block(finished); } self.state = IGListBatchUpdateStateIdle; } #pragma mark - Fallbacks - (void)_reload { [self.delegate listAdapterUpdater:self.updater willReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; [self _applyDataUpdates]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; [self.delegate listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:YES]; [self _executeCompletionAsFinished:YES]; } - (void)_bail { [self.delegate listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; [self _executeCompletionAsFinished:NO]; } #pragma mark - Cancel - (BOOL)cancel { if (_mode != IGListBatchUpdateTransactionModeCancellable) { return NO; } _mode = IGListBatchUpdateTransactionModeCancelled; return YES; } #pragma mark - Item updates - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { [self.inUpdateItemCollector.itemInserts addObjectsFromArray:indexPaths]; } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { [self.inUpdateItemCollector.itemDeletes addObjectsFromArray:indexPaths]; } - (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath]; [self.inUpdateItemCollector.itemMoves addObject:move]; } - (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.inUpdateItemCollector.itemReloads addObject:reload]; } - (void)reloadSections:(NSIndexSet *)sections { [self.inUpdateItemCollector.sectionReloads addIndexes:sections]; } - (void)addCompletionBlock:(IGListUpdatingCompletion)completion { if (!_inUpdateCompletionBlocks) { _inUpdateCompletionBlocks = [NSMutableArray new]; } [_inUpdateCompletionBlocks addObject:completion]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBindingSectionController.h" @interface IGListBindingSectionController (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListBindingSectionController+DebugDescription.h" #import "IGListDebuggingUtilities.h" @implementation IGListBindingSectionController (DebugDescription) - (NSString *)debugDescription { NSMutableArray *lines = [NSMutableArray arrayWithObject:[NSString stringWithFormat:@"IGListBindingSectionController %p:", self]]; [lines addObjectsFromArray:IGListDebugIndentedLines([self debugDescriptionLines])]; return [lines componentsJoinedByString:@"\n"]; } - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; [debug addObject:[NSString stringWithFormat:@"Selection delegate: %@", self.selectionDelegate]]; [debug addObject:[NSString stringWithFormat:@"Object: %@", self.object]]; [debug addObject:@"View models:"]; for (id viewModel in self.viewModels) { [debug addObject:[NSString stringWithFormat:@"%@: %@", viewModel, viewModel.diffIdentifier]]; } [debug addObject:[NSString stringWithFormat:@"Number of items: %ld", (long)self.numberOfItems]]; [debug addObject:[NSString stringWithFormat:@"View controller: %@", self.viewController]]; [debug addObject:[NSString stringWithFormat:@"Collection context: %@", self.collectionContext]]; [debug addObject:[NSString stringWithFormat:@"Section: %ld", (long)self.section]]; [debug addObject:[NSString stringWithFormat:@"Is first section: %@", IGListDebugBOOL(self.isFirstSection)]]; [debug addObject:[NSString stringWithFormat:@"Is last section: %@", IGListDebugBOOL(self.isLastSection)]]; [debug addObject:[NSString stringWithFormat:@"Supplementary view source: %@", self.supplementaryViewSource]]; [debug addObject:[NSString stringWithFormat:@"Display delegate: %@", self.displayDelegate]]; [debug addObject:[NSString stringWithFormat:@"Working range delegate: %@", self.workingRangeDelegate]]; [debug addObject:[NSString stringWithFormat:@"Scroll delegate: %@", self.scrollDelegate]]; #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import static inline CGRect IGListRectIntegralScaled(CGRect rect) { CGFloat scale = [[UIScreen mainScreen] scale]; return CGRectMake(floor(rect.origin.x * scale) / scale, floor(rect.origin.y * scale) / scale, ceil(rect.size.width * scale) / scale, ceil(rect.size.height * scale) / scale); } ================================================ FILE: Source/IGListKit/Internal/IGListDataSourceChangeTransaction.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #import "IGListUpdatingDelegate.h" #import "IGListUpdateTransactable.h" NS_ASSUME_NONNULL_BEGIN /// Handles a `UICollectionView` `dataSource` change IGLK_SUBCLASSING_RESTRICTED @interface IGListDataSourceChangeTransaction : NSObject - (instancetype)initWithChangeBlock:(IGListDataSourceChangeBlock)block itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListDataSourceChangeTransaction.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDataSourceChangeTransaction.h" @implementation IGListDataSourceChangeTransaction { // Given IGListDataSourceChangeBlock _block; NSArray *_itemUpdateBlocks; NSArray *_completionBlocks; // Internal NSMutableArray *_inUpdateCompletionBlocks; IGListBatchUpdateState _state; } - (instancetype)initWithChangeBlock:(IGListDataSourceChangeBlock)block itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks { if (self = [super init]) { _block = block; _itemUpdateBlocks = itemUpdateBlocks; _completionBlocks = completionBlocks; } return self; } - (IGListBatchUpdateState)state { return _state; } #pragma mark - Update - (void)begin { // Item updates must not send mutations to the collection view while we are reloading _state = IGListBatchUpdateStateExecutingBatchUpdateBlock; // Execute all stored item update blocks even if all cells will get reloaded. the actual collection view // mutations will be discarded, but clients are encouraged to put their actual /data/ mutations inside the // update block as well, so if we don't execute the block the changes will never happen for (IGListItemUpdateBlock itemUpdateBlock in _itemUpdateBlocks) { itemUpdateBlock(); } _state = IGListBatchUpdateStateExecutedBatchUpdateBlock; // Apply dataSource change if (_block) { _block(); } for (IGListUpdatingCompletion completion in _completionBlocks) { completion(YES); } // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any // re-entrant updates. NSArray *inUpdateCompletionBlocks = [_inUpdateCompletionBlocks copy]; for (IGListUpdatingCompletion completion in inUpdateCompletionBlocks) { completion(YES); } _state = IGListBatchUpdateStateIdle; } #pragma mark - Cancel - (BOOL)cancel { // This transaction is synchronous return NO; } #pragma mark - Item updates - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { // no-op because changing the UICollectionView's dataSource invalidates section/item counts } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { // no-op because changing the UICollectionView's dataSource invalidates section/item counts } - (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { // no-op because changing the UICollectionView's dataSource invalidates section/item counts } - (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { // no-op because changing the UICollectionView's dataSource invalidates section/item counts } - (void)reloadSections:(NSIndexSet *)sections { // no-op because changing the UICollectionView's dataSource invalidates section/item counts } - (void)addCompletionBlock:(IGListUpdatingCompletion)completion { if (!_inUpdateCompletionBlocks) { _inUpdateCompletionBlocks = [NSMutableArray new]; } [_inUpdateCompletionBlocks addObject:completion]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListDebugger.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif @class IGListAdapter; IGLK_SUBCLASSING_RESTRICTED @interface IGListDebugger : NSObject + (void)trackAdapter:(IGListAdapter *)adapter; + (NSArray *)adapterDescriptions; + (void)clear; + (NSString *)dump; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Source/IGListKit/Internal/IGListDebugger.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDebugger.h" #import "IGListAdapter+DebugDescription.h" @implementation IGListDebugger static NSHashTable *livingAdaptersTable = nil; + (void)trackAdapter:(IGListAdapter *)adapter { #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED if (livingAdaptersTable == nil) { livingAdaptersTable = [NSHashTable weakObjectsHashTable]; } [livingAdaptersTable addObject:adapter]; #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED } + (NSArray *)adapterDescriptions { NSMutableArray *descriptions = [NSMutableArray new]; for (IGListAdapter *adapter in livingAdaptersTable) { [descriptions addObject:[adapter debugDescription]]; } return descriptions; } + (void)clear { [livingAdaptersTable removeAllObjects]; } + (NSString *)dump { return [[self adapterDescriptions] componentsJoinedByString:@"\n"]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListDebuggingUtilities.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NSString *IGListDebugBOOL(BOOL b); NSArray *IGListDebugIndentedLines(NSArray *lines); ================================================ FILE: Source/IGListKit/Internal/IGListDebuggingUtilities.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDebuggingUtilities.h" NSString *IGListDebugBOOL(BOOL b) { return b ? @"Yes" : @"No"; } NSArray *IGListDebugIndentedLines(NSArray *lines) { NSMutableArray *newLines = [NSMutableArray new]; for (NSString *line in lines) { [newLines addObject:[NSString stringWithFormat:@" %@", line]]; } return newLines; } ================================================ FILE: Source/IGListKit/Internal/IGListDefaultExperiments.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if __has_include() #import #else #import "IGListExperiments.h" #endif /// Provides a list of experiments that are enabled by default in IGListKit. static inline IGListExperiment IGListDefaultExperiments(void) { return IGListExperimentThrowOnInconsistencyException; } ================================================ FILE: Source/IGListKit/Internal/IGListDisplayHandler.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif @class IGListAdapter; @class IGListSectionController; @class IGListAdapterDelegateAnnouncer; NS_ASSUME_NONNULL_BEGIN IGLK_SUBCLASSING_RESTRICTED @interface IGListDisplayHandler : NSObject /** Counted set of the currently visible section controllers. */ @property (nonatomic, strong, readonly) NSCountedSet *visibleListSections; /** Tells the handler that a cell will be displayed in the IGListAdapter. @param cell A cell that will be displayed. @param listAdapter The adapter the cell will display in. @param sectionController The section controller that manages the cell. @param object The object that powers the section controller. @param indexPath The index path of the cell in the UICollectionView. */ - (void)willDisplayCell:(UICollectionViewCell *)cell forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath; /** Tells the handler that a cell did end display in the IGListAdapter. @param cell A cell that will be displayed. @param listAdapter The adapter the cell will display in. @param sectionController The section controller that manages the cell. @param indexPath The index path of the cell in the UICollectionView. */ - (void)didEndDisplayingCell:(UICollectionViewCell *)cell forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController indexPath:(NSIndexPath *)indexPath; /** Tells the handler that a supplementary view will be displayed in the IGListAdapter. @param view A supplementary view that will be displayed. @param listAdapter The adapter the supplementary view will display in. @param sectionController The section controller that manages the supplementary view. @param object The object that powers the section controller. @param indexPath The index path of the supplementary view in the UICollectionView. */ - (void)willDisplaySupplementaryView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath; /** Tells the handler that a supplementary view did end display in the IGListAdapter. @param view A supplementary view that will be displayed. @param listAdapter The adapter the supplementary view will display in. @param sectionController The section controller that manages the supplementary view. @param indexPath The index path of the supplementary view in the UICollectionView. */ - (void)didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController indexPath:(NSIndexPath *)indexPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListDisplayHandler.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDisplayHandler.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListAdapterInternal.h" #import "IGListAdapterDelegateAnnouncerInternal.h" #import "IGListDisplayDelegate.h" #import "IGListSectionController.h" #import "IGListSectionControllerInternal.h" @interface IGListDisplayHandler () @property (nonatomic, strong) NSMapTable *visibleViewObjectMap; @end @implementation IGListDisplayHandler - (instancetype)init { if (self = [super init]) { _visibleListSections = [NSCountedSet new]; _visibleViewObjectMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:0]; } return self; } - (id)_pluckObjectForView:(UICollectionReusableView *)view { NSMapTable *viewObjectMap = self.visibleViewObjectMap; id object = [viewObjectMap objectForKey:view]; [viewObjectMap removeObjectForKey:view]; return object; } - (void)_willDisplayReusableView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath { IGParameterAssert(view != nil); IGParameterAssert(listAdapter != nil); IGParameterAssert(object != nil); IGParameterAssert(indexPath != nil); [self.visibleViewObjectMap setObject:object forKey:view]; NSCountedSet *visibleListSections = self.visibleListSections; if ([visibleListSections countForObject:sectionController] == 0) { [sectionController willDisplaySectionControllerWithListAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object atIndex:indexPath.section]; [listAdapter.globalDelegateAnnouncer announceObjectDisplayWithAdapter:listAdapter object:object index:indexPath.section]; } [visibleListSections addObject:sectionController]; } - (void)_didEndDisplayingReusableView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath { IGParameterAssert(view != nil); IGParameterAssert(listAdapter != nil); IGParameterAssert(indexPath != nil); if (object == nil || sectionController == nil) { return; } const NSInteger section = indexPath.section; NSCountedSet *visibleSections = self.visibleListSections; [visibleSections removeObject:sectionController]; if ([visibleSections countForObject:sectionController] == 0) { [sectionController didEndDisplayingSectionControllerWithListAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:section]; [listAdapter.globalDelegateAnnouncer announceObjectEndDisplayWithAdapter:listAdapter object:object index:indexPath.section]; } } - (void)willDisplaySupplementaryView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath { [self _willDisplayReusableView:view forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; } - (void)didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController indexPath:(NSIndexPath *)indexPath { // if cell display events break, don't send display events when the object has disappeared id object = [self _pluckObjectForView:view]; [self _didEndDisplayingReusableView:view forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; } - (void)willDisplayCell:(UICollectionViewCell *)cell forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController object:(id)object indexPath:(NSIndexPath *)indexPath { [sectionController willDisplayCell:cell atIndex:indexPath.item listAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object cell:cell atIndexPath:indexPath]; [listAdapter.globalDelegateAnnouncer announceCellDisplayWithAdapter:listAdapter object:object cell:cell indexPath:indexPath]; [self _willDisplayReusableView:cell forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; } - (void)didEndDisplayingCell:(UICollectionViewCell *)cell forListAdapter:(IGListAdapter *)listAdapter sectionController:(IGListSectionController *)sectionController indexPath:(NSIndexPath *)indexPath { // if cell display events break, don't send cell events to the displayDelegate when the object has disappeared id object = [self _pluckObjectForView:cell]; if (object == nil) { return; } [sectionController didEndDisplayingCell:cell atIndex:indexPath.item listAdapter:listAdapter]; [listAdapter.delegate listAdapter:listAdapter didEndDisplayingObject:object cell:cell atIndexPath:indexPath]; [listAdapter.globalDelegateAnnouncer announceCellEndDisplayWithAdapter:listAdapter object:object cell:cell indexPath:indexPath]; [self _didEndDisplayingReusableView:cell forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListItemUpdatesCollector.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif @class IGListMoveIndexPath; @class IGListReloadIndexPath; /// Object to collect item updates. IGLK_SUBCLASSING_RESTRICTED @interface IGListItemUpdatesCollector : NSObject @property (nonatomic, strong, readonly) NSMutableIndexSet *sectionReloads; @property (nonatomic, strong, readonly) NSMutableArray *itemInserts; @property (nonatomic, strong, readonly) NSMutableArray *itemDeletes; @property (nonatomic, strong, readonly) NSMutableArray *itemReloads; @property (nonatomic, strong, readonly) NSMutableArray *itemMoves; - (BOOL)hasChanges; @end ================================================ FILE: Source/IGListKit/Internal/IGListItemUpdatesCollector.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListItemUpdatesCollector.h" @implementation IGListItemUpdatesCollector - (instancetype)init { if (self = [super init]) { _sectionReloads = [NSMutableIndexSet new]; _itemInserts = [NSMutableArray new]; _itemMoves = [NSMutableArray new]; _itemDeletes = [NSMutableArray new]; _itemReloads = [NSMutableArray new]; } return self; } - (BOOL)hasChanges { return [self.sectionReloads count] > 0 || [self.itemInserts count] > 0 || [self.itemMoves count] > 0 || [self.itemReloads count] > 0 || [self.itemDeletes count] > 0; } @end ================================================ FILE: Source/IGListKit/Internal/IGListPerformDiff.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListExperiments.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN @class IGListTransitionData; @class IGListIndexSetResult; /** @param result The diffing results @param onBackground Whether the diffing ran on a background thread */ NS_SWIFT_NAME(ListDiffExecutorCompletion) typedef void (^IGListDiffExecutorCompletion)(IGListIndexSetResult *result, BOOL onBackground); /** Perform diffing, which can happen sync or async depending on the params given. @param data Contains the objects before and after the update @param view View on which we will perform the update. Used to check visibility. @param allowsBackgroundDiffing Allows the diffing to be performed off the main thread @param adaptiveConfig Details of how the adaptive diffing should work @param completion Returns the diffing results. Can be called async or sync, but will be called on main thread. */ NS_SWIFT_NAME(ListPerformDiff(data:view:allowsBackgroundDiffing:adaptiveConfig:completion:)) FOUNDATION_EXTERN void IGListPerformDiffWithData(IGListTransitionData *_Nullable data, UIView *_Nullable view, BOOL allowsBackgroundDiffing, IGListAdaptiveDiffingExperimentConfig adaptiveConfig, IGListDiffExecutorCompletion completion); NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListPerformDiff.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListPerformDiff.h" #if !__has_include() #import "IGListDiff.h" #else #import #endif #import "IGListTransitionData.h" #import "IGListViewVisibilityTracker.h" #pragma mark - Regular (not adaptive) static void _regularPerformDiffWithData(IGListTransitionData *_Nullable data, BOOL allowsBackground, IGListDiffExecutorCompletion completion) { if (allowsBackground) { dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ IGListIndexSetResult *result = IGListDiff(data.fromObjects, data.toObjects, IGListDiffEquality); dispatch_async(dispatch_get_main_queue(), ^{ completion(result, allowsBackground); }); }); } else { IGListIndexSetResult *result = IGListDiff(data.fromObjects, data.toObjects, IGListDiffEquality); completion(result, allowsBackground); } } #pragma mark - Adaptive static dispatch_queue_t _queueForData(IGListTransitionData *data, UIView *view, BOOL allowsBackground, IGListAdaptiveDiffingExperimentConfig adaptiveConfig) { if (!allowsBackground) { return dispatch_get_main_queue(); } if (adaptiveConfig.lowerPriorityWhenViewNotVisible) { IGListViewVisibilityTracker *const tracker = IGListViewVisibilityTrackerAttachedOnView(view); if (tracker && tracker.state == IGListViewVisibilityStateNotVisible) { return dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0); } } // If we don't have a lot of items, the dispatching back and forth can add unnecessary delay. if ((NSInteger)data.fromObjects.count < adaptiveConfig.maxItemCountToRunOnMain && (NSInteger)data.toObjects.count < adaptiveConfig.maxItemCountToRunOnMain) { return dispatch_get_main_queue(); } const intptr_t qos = adaptiveConfig.higherQOSEnabled ? QOS_CLASS_USER_INTERACTIVE : QOS_CLASS_USER_INITIATED; return dispatch_get_global_queue(qos, 0); } static void _adaptivePerformDiffWithData(IGListTransitionData *_Nullable data, UIView *view, BOOL allowsBackground, IGListAdaptiveDiffingExperimentConfig adaptiveConfig, IGListDiffExecutorCompletion completion) { const dispatch_queue_t queue = _queueForData(data, view, allowsBackground, adaptiveConfig); if (queue == dispatch_get_main_queue() && [NSThread isMainThread]) { IGListIndexSetResult *const result = IGListDiff(data.fromObjects, data.toObjects, IGListDiffEquality); completion(result, NO); } else { dispatch_async(queue, ^{ IGListIndexSetResult *const result = IGListDiff(data.fromObjects, data.toObjects, IGListDiffEquality); dispatch_async(dispatch_get_main_queue(), ^{ completion(result, YES); }); }); } } #pragma mark - Public void IGListPerformDiffWithData(IGListTransitionData *_Nullable data, UIView *view, BOOL allowsBackground, IGListAdaptiveDiffingExperimentConfig adaptiveConfig, IGListDiffExecutorCompletion completion) { if (!completion) { return; } if (adaptiveConfig.enabled) { _adaptivePerformDiffWithData(data, view, allowsBackground, adaptiveConfig, completion); } else { // Just to be safe, lets keep the original code path intact while adaptive diffing is still an experiment. _regularPerformDiffWithData(data, allowsBackground, completion); } } ================================================ FILE: Source/IGListKit/Internal/IGListReloadIndexPath.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif NS_ASSUME_NONNULL_BEGIN /** An object with index path information for reloading an item during a batch update. */ IGLK_SUBCLASSING_RESTRICTED @interface IGListReloadIndexPath : NSObject /** The index path of the item before batch updates are applied. */ @property (nonatomic, strong, readonly) NSIndexPath *fromIndexPath; /** The index path of the item after batch updates are applied. */ @property (nonatomic, strong, readonly) NSIndexPath *toIndexPath; /** Creates a new reload object. @param fromIndexPath The index path of the item before batch updates. @param toIndexPath The index path of the item after batch updates. @return A new reload object. */ - (instancetype)initWithFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath NS_DESIGNATED_INITIALIZER; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListReloadIndexPath.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListReloadIndexPath.h" @implementation IGListReloadIndexPath - (instancetype)initWithFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { if (self = [super init]) { _fromIndexPath = fromIndexPath; _toIndexPath = toIndexPath; } return self; } @end ================================================ FILE: Source/IGListKit/Internal/IGListReloadTransaction.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #import "IGListUpdatingDelegate.h" #import "IGListUpdateTransactable.h" @class IGListAdapterUpdater; @protocol IGListAdapterUpdaterDelegate; NS_ASSUME_NONNULL_BEGIN /// Handles a full reload transaction IGLK_SUBCLASSING_RESTRICTED @interface IGListReloadTransaction : NSObject - (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock updater:(IGListAdapterUpdater *)updater delegate:(nullable id)delegate reloadBlock:(IGListReloadUpdateBlock)reloadBlock itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListReloadTransaction.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListReloadTransaction.h" #import "IGListAdapterUpdaterDelegate.h" @interface IGListReloadTransaction () // Given @property (nonatomic, copy, readonly) UICollectionView *collectionView; @property (nonatomic, weak, readonly) IGListAdapterUpdater *updater; @property (nonatomic, weak, readonly, nullable) id delegate; @property (nonatomic, copy, readonly) IGListReloadUpdateBlock reloadBlock; @property (nonatomic, copy, readonly) NSArray *itemUpdateBlocks; @property (nonatomic, copy, readonly) NSArray *completionBlocks; // Internal @property (nonatomic, assign, readwrite) IGListBatchUpdateState state; @property (nonatomic, copy, readonly) NSMutableArray *inUpdateCompletionBlocks; @end @implementation IGListReloadTransaction - (instancetype)initWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock updater:(IGListAdapterUpdater *)updater delegate:(id)delegate reloadBlock:(IGListReloadUpdateBlock)reloadBlock itemUpdateBlocks:(NSArray *)itemUpdateBlocks completionBlocks:(NSArray *)completionBlocks { if (self = [super init]) { _collectionView = collectionViewBlock ? collectionViewBlock() : nil; _updater = updater; _delegate = delegate; _reloadBlock = [reloadBlock copy]; _itemUpdateBlocks = [itemUpdateBlocks copy]; _completionBlocks = [completionBlocks copy]; _state = IGListBatchUpdateStateIdle; } return self; } #pragma mark - Update - (void)begin { // bail early if the collection view has been deallocated in the time since the update was queued if (self.collectionView == nil) { [self.delegate listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; [self _executeCompletionBlocks:YES]; return; } // item updates must not send mutations to the collection view while we are reloading self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; if (self.reloadBlock) { self.reloadBlock(); } // execute all stored item update blocks even if we are just calling reloadData. the actual collection view // mutations will be discarded, but clients are encouraged to put their actual /data/ mutations inside the // update block as well, so if we don't execute the block the changes will never happen for (IGListItemUpdateBlock itemUpdateBlock in self.itemUpdateBlocks) { itemUpdateBlock(); } self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; [self.delegate listAdapterUpdater:self.updater willReloadDataWithCollectionView:self.collectionView isFallbackReload:NO]; [self.collectionView reloadData]; [self.collectionView.collectionViewLayout invalidateLayout]; [self.collectionView layoutIfNeeded]; [self.delegate listAdapterUpdater:self.updater didReloadDataWithCollectionView:self.collectionView isFallbackReload:NO]; [self _executeCompletionBlocks:YES]; } - (void)_executeCompletionBlocks:(BOOL)finished { for (IGListUpdatingCompletion block in self.completionBlocks) { block(finished); } // Execute any completion blocks from item updates. Added after item blocks are executed in order to capture any // re-entrant updates. NSArray *inUpdateCompletionBlocks = [self.inUpdateCompletionBlocks copy]; for (IGListUpdatingCompletion block in inUpdateCompletionBlocks) { block(finished); } self.state = IGListBatchUpdateStateIdle; } #pragma mark - Cancel - (BOOL)cancel { // This transaction is syncronous return NO; } #pragma mark - Item updates - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { // no-op. Reloading all cells. } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { // no-op. Reloading all cells. } - (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { // no-op. Reloading all cells. } - (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { // no-op. Reloading all cells. } - (void)reloadSections:(NSIndexSet *)sections { // no-op. Reloading all cells. } - (void)addCompletionBlock:(IGListUpdatingCompletion)completion { if (!self.inUpdateCompletionBlocks) { _inUpdateCompletionBlocks = [NSMutableArray new]; } [self.inUpdateCompletionBlocks addObject:completion]; } @end ================================================ FILE: Source/IGListKit/Internal/IGListSectionControllerInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListSectionController.h" FOUNDATION_EXTERN void IGListSectionControllerPushThread(UIViewController *viewController, id collectionContext); FOUNDATION_EXTERN void IGListSectionControllerPopThread(void); @interface IGListSectionController() @property (nonatomic, weak, readwrite) id collectionContext; @property (nonatomic, weak, readwrite) UIViewController *viewController; @property (nonatomic, assign, readwrite) NSInteger section; @property (nonatomic, assign, readwrite) BOOL isFirstSection; @property (nonatomic, assign, readwrite) BOOL isLastSection; /* Provides a way for specialized section controllers (like the stacked section controller) to reject invalid moves */ - (BOOL)canMoveItemAtIndex:(NSInteger)sourceItemIndex toIndex:(NSInteger)destinationItemIndex; - (void)willDisplayCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter; - (void)didEndDisplayingCell:(UICollectionViewCell *)cell atIndex:(NSInteger)index listAdapter:(IGListAdapter *)listAdapter; - (void)willDisplaySectionControllerWithListAdapter:(IGListAdapter *)listAdapter; - (void)didEndDisplayingSectionControllerWithListAdapter:(IGListAdapter *)listAdapter; @end ================================================ FILE: Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListSectionMap.h" @interface IGListSectionMap (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListSectionMap+DebugDescription.h" #import "IGListBindingSectionController.h" @implementation IGListSectionMap (DebugDescription) - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [self enumerateUsingBlock:^(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop) { if ([sectionController isKindOfClass:[IGListBindingSectionController class]]) { [debug addObject:[sectionController debugDescription]]; } else { [debug addObject:[NSString stringWithFormat:@"Object and section controller at section: %li:", (long)section]]; [debug addObject:[NSString stringWithFormat:@" %@", object]]; [debug addObject:[NSString stringWithFormat:@" %@", sectionController]]; } }]; #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/IGListSectionMap.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif @class IGListSectionController; @protocol IGListDiffable; NS_ASSUME_NONNULL_BEGIN /** The IGListSectionMap provides a way to map a collection of objects to a collection of section controllers and achieve constant-time lookups O(1). IGListSectionMap is a mutable object and does not guarantee thread safety. */ IGLK_SUBCLASSING_RESTRICTED @interface IGListSectionMap : NSObject /** @param mapTable Table used to keep a relationship between the object and its section-controller */ - (instancetype)initWithMapTable:(NSMapTable, IGListSectionController *> *)mapTable NS_DESIGNATED_INITIALIZER; /** The objects stored in the map. */ @property (nonatomic, strong, readonly) NSArray> *objects; /** Update the map with objects and the section controller counterparts. @param objects The objects in the collection. @param sectionControllers The section controllers that map to each object. */ - (void)updateWithObjects:(NSArray> *)objects sectionControllers:(NSArray *)sectionControllers; /** Fetch a section controller given a section. @param section The section index of the section controller. @return A section controller. */ - (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section; /** Fetch the object for a section @param section The section index of the object. @return The object corresponding to the section. */ - (nullable id)objectForSection:(NSInteger)section; /** Fetch a section controller given an object. Can return nil. @param object The object that maps to a section controller. @return A section controller. */ - (nullable IGListSectionController *)sectionControllerForObject:(id)object; /** Look up the section index for a section controller. @param sectionController The list to look up. @return The section index of the given section controller if it exists, NSNotFound otherwise. */ - (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController; /** Look up the section index for an object. @param object The object to look up. @return The section index of the given object if it exists, NSNotFound otherwise. */ - (NSInteger)sectionForObject:(id)object; /** Remove all saved objects and section controllers. */ - (void)reset; /** Update an object with a new instance. */ - (void)updateObject:(id)object; /** Applies a given block object to the entries of the section controller map. @param block A block object to operate on entries in the section controller map. */ - (void)enumerateUsingBlock:(void (^)(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop))block; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListSectionMap.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListSectionMap.h" #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListSectionControllerInternal.h" @interface IGListSectionMap () // both of these maps allow fast lookups of objects, list objects, and indexes @property (nonatomic, strong, readonly, nonnull) NSMapTable, IGListSectionController *> *objectToSectionControllerMap; @property (nonatomic, strong, readonly, nonnull) NSMapTable *sectionControllerToSectionMap; @property (nonatomic, strong, nonnull) NSMutableArray> *mObjects; @property (nonatomic, strong, nullable) NSMutableArray> *diffIdentifiersSnapshot; @end @implementation IGListSectionMap - (instancetype)initWithMapTable:(NSMapTable, IGListSectionController *> *)mapTable { IGParameterAssert(mapTable != nil); if (self = [super init]) { _objectToSectionControllerMap = [mapTable copy]; // lookup list objects by pointer equality _sectionControllerToSectionMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory | NSMapTableObjectPointerPersonality valueOptions:NSMapTableStrongMemory capacity:0]; _mObjects = [NSMutableArray new]; } return self; } #pragma mark - Public API - (NSArray> *)objects { return [self.mObjects copy]; } - (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController { IGParameterAssert(sectionController != nil); NSNumber *index = [self.sectionControllerToSectionMap objectForKey:sectionController]; return index != nil ? [index integerValue] : NSNotFound; } - (IGListSectionController *)sectionControllerForSection:(NSInteger)section { return [self.objectToSectionControllerMap objectForKey:[self objectForSection:section]]; } - (void)updateWithObjects:(NSArray> *)objects sectionControllers:(NSArray *)sectionControllers { IGParameterAssert(objects.count == sectionControllers.count); [self reset]; [self _validateAllDiffIdentifiers]; self.mObjects = [objects mutableCopy]; [self _updateAllDiffIdentifiers]; id firstObject = objects.firstObject; id lastObject = objects.lastObject; [objects enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) { IGListSectionController *sectionController = sectionControllers[idx]; // set the index of the list for easy reverse lookup [self.sectionControllerToSectionMap setObject:@(idx) forKey:sectionController]; [self.objectToSectionControllerMap setObject:sectionController forKey:object]; sectionController.isFirstSection = (object == firstObject); sectionController.isLastSection = (object == lastObject); sectionController.section = (NSInteger)idx; }]; } - (nullable IGListSectionController *)sectionControllerForObject:(id)object { IGParameterAssert(object != nil); return [self.objectToSectionControllerMap objectForKey:object]; } - (nullable id)objectForSection:(NSInteger)section { if (section < 0) { return nil; } NSArray *objects = self.mObjects; if ((NSUInteger)section >= objects.count) { return nil; } return objects[section]; } - (NSInteger)sectionForObject:(id)object { if (object == nil) { return NSNotFound; } id sectionController = [self sectionControllerForObject:object]; if (sectionController == nil) { return NSNotFound; } return [self sectionForSectionController:sectionController]; } - (void)reset { [self enumerateUsingBlock:^(id _Nonnull object, IGListSectionController * _Nonnull sectionController, NSInteger section, BOOL * _Nonnull stop) { sectionController.section = NSNotFound; sectionController.isFirstSection = NO; sectionController.isLastSection = NO; }]; [self.sectionControllerToSectionMap removeAllObjects]; [self.objectToSectionControllerMap removeAllObjects]; } - (void)updateObject:(id)object { IGParameterAssert(object != nil); const NSInteger section = [self sectionForObject:object]; id sectionController = [self sectionControllerForObject:object]; [self.sectionControllerToSectionMap setObject:@(section) forKey:sectionController]; [self.objectToSectionControllerMap setObject:sectionController forKey:object]; [self _validateDiffIdentifierAtSection:section]; self.mObjects[section] = object; [self _updateDiffIdentifierAtSection:section newObject:object]; } - (void)enumerateUsingBlock:(void (^)(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop))block { IGParameterAssert(block != nil); BOOL stop = NO; NSArray *objects = self.objects; for (NSInteger section = 0; section < (NSInteger)objects.count; section++) { id object = objects[section]; IGListSectionController *sectionController = [self sectionControllerForObject:object]; if (sectionController != nil) { block(object, sectionController, section, &stop); if (stop) { break; } } } } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { IGListSectionMap *copy = [[IGListSectionMap allocWithZone:zone] initWithMapTable:self.objectToSectionControllerMap]; if (copy != nil) { copy->_sectionControllerToSectionMap = [self.sectionControllerToSectionMap copy]; copy->_mObjects = [self.mObjects mutableCopy]; copy->_diffIdentifiersSnapshot = [self.diffIdentifiersSnapshot mutableCopy]; } return copy; } #pragma mark - Diff Identifiers validation #if IG_ASSERTIONS_ENABLED static void IGListSectionMapValidateDiffIdentifier(NSUInteger section, NSArray> *mObjects, NSArray> *_Nullable diffIdentifiersSnapshot) { if (mObjects.count != diffIdentifiersSnapshot.count) { // Don't have an accurate snapshot of the diff identifiers. return; } if (section < 0 || section >= mObjects.count) { return; } id const object = mObjects[section]; id const newDiffIdentifier = [object diffIdentifier]; id const oldDiffIdentifier = diffIdentifiersSnapshot[section]; // Between updates, we don't expect the diffIdentifier to change for the same section. If it does, we lose our ability to find the // corresponding section-controller in `objectToSectionControllerMap` and usually crash. For example: // - Section has suddently 0 items, so batch updates are wrong // - Adapter returns nil cell // The fix is to make sure -diffIdentifier is not mutable. Generally, -diffIdentifier should be pretty simple (like a UUID) // and -isEqualToDiffableObject should be where we compare all other relevant properties to trigger an update. IGAssert([oldDiffIdentifier isEqual:newDiffIdentifier], @"Diff identifier changed for object %@ at section %i, from %@ to %@", NSStringFromClass([(NSObject *)object class]), (unsigned int)section, oldDiffIdentifier, newDiffIdentifier); } #endif - (void)_validateAllDiffIdentifiers { #if IG_ASSERTIONS_ENABLED for (NSUInteger section = 0; section < _mObjects.count; section++) { IGListSectionMapValidateDiffIdentifier(section, _mObjects, _diffIdentifiersSnapshot); } #endif } - (void)_validateDiffIdentifierAtSection:(NSInteger)section { #if IG_ASSERTIONS_ENABLED IGListSectionMapValidateDiffIdentifier(section, _mObjects, _diffIdentifiersSnapshot); #endif } - (void)_updateAllDiffIdentifiers { #if IG_ASSERTIONS_ENABLED if (!_diffIdentifiersSnapshot) { _diffIdentifiersSnapshot = [NSMutableArray new]; } [_diffIdentifiersSnapshot removeAllObjects]; for (id object in _mObjects) { [_diffIdentifiersSnapshot addObject:[object diffIdentifier]]; } #endif } - (void)_updateDiffIdentifierAtSection:(NSInteger)section newObject:(id)newObject { #if IG_ASSERTIONS_ENABLED _diffIdentifiersSnapshot[section] = newObject.diffIdentifier; #endif } @end ================================================ FILE: Source/IGListKit/Internal/IGListUpdateCoalescer.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if __has_include() #import #else #import "IGListExperiments.h" #endif NS_ASSUME_NONNULL_BEGIN @class IGListUpdateCoalescer; @protocol IGListUpdateCoalescerDelegate /// Indicates that coalescing is done and the update should be performed. - (void)performUpdateWithCoalescer:(IGListUpdateCoalescer *)coalescer; @end /** Class responsible for batching updates together */ NS_SWIFT_NAME(ListUpdateCoalescer) @interface IGListUpdateCoalescer : NSObject @property (nonatomic, assign) IGListAdaptiveCoalescingExperimentConfig adaptiveCoalescingExperimentConfig; @property (nonatomic, weak) id delegate; /** Start coalescing updates, which will eventually call `-performUpdateWithCoalescer` @params view View used to track visibility (if enabled in config) */ - (void)queueUpdateForView:(nullable UIView *)view; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListUpdateCoalescer.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListUpdateCoalescer.h" #import "IGListViewVisibilityTracker.h" @implementation IGListUpdateCoalescer { BOOL _hasQueuedUpdate; // Adaptive NSDate *_lastUpdateStartDate; NSTimeInterval _coalescenceInterval; } - (void)queueUpdateForView:(nullable UIView *)view { if (_hasQueuedUpdate) { return; } // dispatch_async to give the main queue time to collect more batch updates so that a minimum amount of work // (diffing, etc) is done on main. dispatch_async does not garauntee a full runloop turn will pass though. // see -performUpdateWithCollectionViewBlock:animated:sectionDataBlock:applySectionDataBlock:completion: for more // details on how coalescence is done. if (self.adaptiveCoalescingExperimentConfig.enabled) { [self _adaptiveDispatchUpdateForView:view]; } else { [self _regularDispatchUpdate]; } } - (void)_regularDispatchUpdate { _hasQueuedUpdate = YES; __weak __typeof__(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf _performUpdate]; }); } static BOOL _isViewVisible(UIView *_Nullable view, IGListAdaptiveCoalescingExperimentConfig config) { if (config.useMaxIntervalWhenViewNotVisible) { IGListViewVisibilityTracker *const tracker = IGListViewVisibilityTrackerAttachedOnView((UIView *)view); if (tracker && tracker.state == IGListViewVisibilityStateNotVisible) { return NO; } } return YES; } - (void)_adaptiveDispatchUpdateForView:(nullable UIView *)view { const IGListAdaptiveCoalescingExperimentConfig config = _adaptiveCoalescingExperimentConfig; const NSTimeInterval timeSinceLastUpdate = -[_lastUpdateStartDate timeIntervalSinceNow]; const BOOL isViewVisible = _isViewVisible(view, config); const NSTimeInterval currentCoalescenceInterval = _coalescenceInterval; if (isViewVisible) { if (!_lastUpdateStartDate || timeSinceLastUpdate > currentCoalescenceInterval) { // It's been long enough, so lets reset interval and perform update right away _coalescenceInterval = config.minInterval; [self _performUpdate]; return; } else { // If we keep hitting the delay, lets increase it. _coalescenceInterval = MIN(currentCoalescenceInterval + config.intervalIncrement, config.maxInterval); } } // Delay by the time remaining in the interval const NSTimeInterval remainingTime = isViewVisible ? (currentCoalescenceInterval - timeSinceLastUpdate) : config.maxInterval; const NSTimeInterval remainingTimeCapped = MAX(remainingTime, 0); _hasQueuedUpdate = YES; __weak __typeof__(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(remainingTimeCapped * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf _performUpdate]; }); } - (void)_performUpdate { _hasQueuedUpdate = NO; _lastUpdateStartDate = [NSDate date]; [self.delegate performUpdateWithCoalescer:self]; } #pragma mark - Properties - (void)setAdaptiveCoalescingExperimentConfig:(IGListAdaptiveCoalescingExperimentConfig)adaptiveCoalescingExperimentConfig { _adaptiveCoalescingExperimentConfig = adaptiveCoalescingExperimentConfig; _coalescenceInterval = adaptiveCoalescingExperimentConfig.minInterval; } @end ================================================ FILE: Source/IGListKit/Internal/IGListUpdateTransactable.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListExperiments.h" #else #import #endif #import "IGListBatchUpdateState.h" #import "IGListUpdatingDelegate.h" NS_ASSUME_NONNULL_BEGIN /// Config to customize how the transition works. typedef struct { BOOL sectionMovesAsDeletesInserts; BOOL singleItemSectionUpdates; BOOL preferItemReloadsForSectionReloads; BOOL allowsReloadingOnTooManyUpdates; BOOL allowsBackgroundDiffing; IGListExperiment experiments; IGListAdaptiveDiffingExperimentConfig adaptiveDiffingExperimentConfig; } IGListUpdateTransactationConfig; /// Conform to this protocol to handle an update transaction. @protocol IGListUpdateTransactable /// Begin the transaction. We expect all completion blocks to be called once finished. - (void)begin; /// Cancel any on going updates. - (BOOL)cancel; /// Current state of the transaction - (IGListBatchUpdateState)state; /// Add a completion block to complete once the transaction ends - (void)addCompletionBlock:(IGListUpdatingCompletion)completion; - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths; - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths; - (void)moveItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; - (void)reloadItemFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; - (void)reloadSections:(NSIndexSet *)sections; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListUpdateTransactionBuilder.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #if !__has_include() #import "IGListMacros.h" #else #import #endif #if !__has_include() #import "IGListUpdatingDelegate.h" #else #import #endif #import "IGListUpdateTransactable.h" @class IGListAdapterUpdater; @protocol IGListAdapterUpdaterDelegate; NS_ASSUME_NONNULL_BEGIN /// Class to collect reload & update information before actually starting the transition. IGLK_SUBCLASSING_RESTRICTED @interface IGListUpdateTransactionBuilder : NSObject /** Add a section-level update. @param animated A flag indicating if the transition should be animated. @param collectionViewBlock A block returning the collecion view to perform updates on. @param sectionDataBlock A block which returns the transition data @param applySectionDataBlock A block that applies the data passed from the `sectionDataBlock` block @param completion A completion block to execute when the update is finished. */ - (void)addSectionBatchUpdateAnimated:(BOOL)animated collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(nullable IGListUpdatingCompletion)completion; /** Add a item-level update. @param animated A flag indicating if the transition should be animated. @param collectionViewBlock A block returning the collecion view to perform updates on. @param itemUpdates A block containing all of the updates. @param completion A completion block to execute when the update is finished. */ - (void)addItemBatchUpdateAnimated:(BOOL)animated collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock itemUpdates:(IGListItemUpdateBlock)itemUpdates completion:(nullable IGListUpdatingCompletion)completion; /** Completely reload data in the collection. @param collectionViewBlock A block returning the collecion view to reload. @param reloadBlock A block that must be called when the adapter reloads the collection view. @param completion A completion block to execute when the reload is finished. */ - (void)addReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadBlock:(IGListReloadUpdateBlock)reloadBlock completion:(nullable IGListUpdatingCompletion)completion; /** Change the `UICollectionView` dataSource @param block A block that applies a `UICollectionView` dataSource change */ - (void)addDataSourceChange:(IGListDataSourceChangeBlock)block; /** Add the changes from another builder. @param builder Add the changes from this builder */ - (void)addChangesFromBuilder:(IGListUpdateTransactionBuilder *)builder; /** Build a transaction based on the changes addded. */ - (nullable id)buildWithConfig:(IGListUpdateTransactationConfig)config delegate:(nullable id)delegate updater:(IGListAdapterUpdater *)updater; - (BOOL)hasChanges; - (nullable UICollectionView *)collectionView; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListUpdateTransactionBuilder.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListUpdateTransactionBuilder.h" #import "IGListBatchUpdateTransaction.h" #import "IGListDataSourceChangeTransaction.h" #import "IGListReloadTransaction.h" /** Modes in ascending order of priority. */ typedef NS_ENUM (NSInteger, IGListUpdateTransactionBuilderMode) { /// The lowest priority is a batch-update, because a reload or dataSource take care of any changes. IGListUpdateTransactionBuilderModeBatchUpdate, /// The second priority is reloading all data. IGListUpdateTransactionBuilderModeReload, /// The highest priority is changing the `UICollectionView` dataSource. IGListUpdateTransactionBuilderModeDataSourceChange, }; @interface IGListUpdateTransactionBuilder () // Batch updates @property (nonatomic, copy, readwrite, nullable) IGListTransitionDataBlock sectionDataBlock; @property (nonatomic, copy, readwrite, nullable) IGListTransitionDataApplyBlock applySectionDataBlock; @property (nonatomic, strong, readonly) NSMutableArray *itemUpdateBlocks; @property (nonatomic, assign, readwrite) BOOL animated; // Reload @property (nonatomic, copy, readwrite, nullable) IGListReloadUpdateBlock reloadBlock; // DataSource change @property (nonatomic, copy, readwrite, nullable) IGListDataSourceChangeBlock dataSourceChangeBlock; // Both @property (nonatomic, assign, readwrite) IGListUpdateTransactionBuilderMode mode; @property (nonatomic, copy, readwrite, nullable) IGListCollectionViewBlock collectionViewBlock; @property (nonatomic, strong, readonly) NSMutableArray *completionBlocks; @end @implementation IGListUpdateTransactionBuilder - (instancetype)init { if (self = [super init]) { _animated = YES; _itemUpdateBlocks = [NSMutableArray new]; _completionBlocks = [NSMutableArray new]; } return self; } #pragma mark - Add changes - (void)addSectionBatchUpdateAnimated:(BOOL)animated collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock sectionDataBlock:(IGListTransitionDataBlock)sectionDataBlock applySectionDataBlock:(IGListTransitionDataApplyBlock)applySectionDataBlock completion:(IGListUpdatingCompletion)completion { self.mode = MAX(self.mode, IGListUpdateTransactionBuilderModeBatchUpdate); // disabled animations will always take priority // reset to YES in -cleanupState self.animated = self.animated && animated; self.collectionViewBlock = collectionViewBlock; // will call the sectionDataBlock after the dispatch self.sectionDataBlock = sectionDataBlock; // always use the last update block, even though this should always do the exact same thing self.applySectionDataBlock = applySectionDataBlock; IGListUpdatingCompletion localCompletion = completion; if (localCompletion) { [self.completionBlocks addObject:localCompletion]; } } - (void)addItemBatchUpdateAnimated:(BOOL)animated collectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock itemUpdates:(IGListItemUpdateBlock)itemUpdates completion:(nullable IGListUpdatingCompletion)completion { self.mode = MAX(self.mode, IGListUpdateTransactionBuilderModeBatchUpdate); // disabled animations will always take priority // reset to YES in -cleanupState self.animated = self.animated && animated; self.collectionViewBlock = collectionViewBlock; [self.itemUpdateBlocks addObject:itemUpdates]; IGListUpdatingCompletion localCompletion = completion; if (localCompletion) { [self.completionBlocks addObject:localCompletion]; } } - (void)addReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadBlock:(IGListReloadUpdateBlock)reloadBlock completion:(nullable IGListUpdatingCompletion)completion { self.mode = MAX(self.mode, IGListUpdateTransactionBuilderModeReload); self.collectionViewBlock = collectionViewBlock; self.reloadBlock = reloadBlock; IGListUpdatingCompletion localCompletion = completion; if (localCompletion) { [self.completionBlocks addObject:localCompletion]; } } - (void)addDataSourceChange:(IGListDataSourceChangeBlock)block { self.mode = MAX(self.mode, IGListUpdateTransactionBuilderModeDataSourceChange); self.dataSourceChangeBlock = block; } - (void)addChangesFromBuilder:(IGListUpdateTransactionBuilder *)builder { if (!builder) { return; } self.mode = MAX(self.mode, builder.mode); // Section update self.animated = self.animated && builder.animated; self.sectionDataBlock = self.sectionDataBlock ?: builder.sectionDataBlock; self.applySectionDataBlock = self.applySectionDataBlock ?: builder.applySectionDataBlock; // Item updates [self.itemUpdateBlocks addObjectsFromArray:builder.itemUpdateBlocks]; // Reload self.reloadBlock = self.reloadBlock ?: builder.reloadBlock; // All self.collectionViewBlock = self.collectionViewBlock ?: builder.collectionViewBlock; [self.completionBlocks addObjectsFromArray:builder.completionBlocks]; } - (nullable id)buildWithConfig:(IGListUpdateTransactationConfig)config delegate:(nullable id)delegate updater:(IGListAdapterUpdater *)updater { switch (self.mode) { case IGListUpdateTransactionBuilderModeBatchUpdate: { IGListCollectionViewBlock collectionViewBlock = self.collectionViewBlock; if (!collectionViewBlock) { return nil; } return [[IGListBatchUpdateTransaction alloc] initWithCollectionViewBlock:collectionViewBlock updater:updater delegate:delegate config:config animated:self.animated sectionDataBlock:self.sectionDataBlock applySectionDataBlock:self.applySectionDataBlock itemUpdateBlocks:self.itemUpdateBlocks completionBlocks:self.completionBlocks]; } case IGListUpdateTransactionBuilderModeReload: { IGListReloadUpdateBlock reloadBlock = self.reloadBlock; IGListCollectionViewBlock collectionViewBlock = self.collectionViewBlock; if (!reloadBlock || !collectionViewBlock) { return nil; } return [[IGListReloadTransaction alloc] initWithCollectionViewBlock:collectionViewBlock updater:updater delegate:delegate reloadBlock:reloadBlock itemUpdateBlocks:self.itemUpdateBlocks completionBlocks:self.completionBlocks]; } case IGListUpdateTransactionBuilderModeDataSourceChange: { IGListDataSourceChangeBlock dataSourceChangeBlock = self.dataSourceChangeBlock; if (!dataSourceChangeBlock) { return nil; } return [[IGListDataSourceChangeTransaction alloc] initWithChangeBlock:dataSourceChangeBlock itemUpdateBlocks:self.itemUpdateBlocks completionBlocks:self.completionBlocks]; } default: /* unexpected */ IGLK_UNEXPECTED_SWITCH_CASE_ABORT(IGListUpdateTransactionBuilderMode, self.mode); } } - (BOOL)hasChanges { return self.mode == IGListUpdateTransactionBuilderModeReload || self.mode == IGListUpdateTransactionBuilderModeDataSourceChange || self.itemUpdateBlocks.count > 0 || self.sectionDataBlock != nil; } - (nullable UICollectionView *)collectionView { return self.collectionViewBlock ? self.collectionViewBlock() : nil; } @end ================================================ FILE: Source/IGListKit/Internal/IGListViewVisibilityTracker.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListViewVisibilityTracker; NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(ListViewVisibilityState) typedef NS_ENUM(NSInteger, IGListViewVisibilityState) { /// View is not in the window, hidden, or has alpha 0 IGListViewVisibilityStateNotVisible, /// View is not visible, but tracking started recently, so it could change soon. IGListViewVisibilityStateNotVisibleEarly, /// View is in the window and not hidden, but there is no guarantee that its bounds are visible or not obstructed IGListViewVisibilityStateMaybeVisible, }; /** Get the tracker associated with a view. If non exists, it will create one and attach it. @param view View's who's visibility is being tracked @return The tracker */ NS_SWIFT_NAME(IGListViewVisibilityTracker(attachedOnView:)) FOUNDATION_EXTERN IGListViewVisibilityTracker *_Nullable IGListViewVisibilityTrackerAttachedOnView(UIView *view); /// Track a view visibility status NS_SWIFT_NAME(ListViewVisibilityTracker) @interface IGListViewVisibilityTracker : NSObject - (instancetype)initWithView:(UIView *)view NS_DESIGNATED_INITIALIZER; /** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** :nodoc: */ + (instancetype)new NS_UNAVAILABLE; /// Calculates the state - (IGListViewVisibilityState)state; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListViewVisibilityTracker.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListViewVisibilityTrackerInternal.h" #import static void * kIGListViewVisibilityTrackerKey = &kIGListViewVisibilityTrackerKey; IGListViewVisibilityTracker *IGListViewVisibilityTrackerAttachedOnView(UIView *view) { if (!view) { return nil; } IGListViewVisibilityTracker *tracker = (IGListViewVisibilityTracker *)objc_getAssociatedObject(view, kIGListViewVisibilityTrackerKey); if (!tracker) { tracker = [[IGListViewVisibilityTracker alloc] initWithView:view]; objc_setAssociatedObject(view, kIGListViewVisibilityTrackerKey, tracker, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return tracker; } @implementation IGListViewVisibilityTracker { __weak UIView *_view; } - (instancetype)initWithView:(UIView *)view { if (self = [super init]) { _view = view; _earlyTimeInterval = 1.0; _dateCreated = [NSDate date]; } return self; } static BOOL _isViewVisible(UIView *view) { if (!view.window) { return NO; } UIView *currentView = view; while (currentView != nil) { if (currentView.hidden || currentView.alpha < FLT_EPSILON) { return NO; } currentView = currentView.superview; } return YES; } - (IGListViewVisibilityState)state { if (_isViewVisible(_view)) { return IGListViewVisibilityStateMaybeVisible; } NSDate *const compareDate = _comparedDateOverride ?: [NSDate date]; const NSTimeInterval timeSinceCreation = [compareDate timeIntervalSinceDate:_dateCreated]; return (timeSinceCreation < _earlyTimeInterval) ? IGListViewVisibilityStateNotVisibleEarly : IGListViewVisibilityStateNotVisible; } @end ================================================ FILE: Source/IGListKit/Internal/IGListViewVisibilityTrackerInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListViewVisibilityTracker.h" NS_ASSUME_NONNULL_BEGIN @interface IGListViewVisibilityTracker() /// Date the tracker is created @property (nonatomic, strong, readwrite) NSDate *dateCreated; /// When evaluating the state, compare the `dateCreated` to this one. If nil, we use `now` @property (nonatomic, strong, readwrite, nullable) NSDate *comparedDateOverride; /// What we consider as early. Default is 1 second. @property (nonatomic, assign, readwrite) NSTimeInterval earlyTimeInterval; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/IGListWorkingRangeHandler.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; @interface IGListWorkingRangeHandler : NSObject /** Initializes the working range handler. @param workingRangeSize the number of sections beyond the visible viewport that should be considered within the working range. Applies equally in both directions above and below the viewport. */ - (instancetype)initWithWorkingRangeSize:(NSInteger)workingRangeSize; /** Tells the handler that a cell will be displayed in the IGListKit infra. @param indexPath The index path of the cell in the UICollectionView. @param listAdapter The adapter managing the infra. */ - (void)willDisplayItemAtIndexPath:(NSIndexPath *)indexPath forListAdapter:(IGListAdapter *)listAdapter; /** Tells the handler that a cell did end display in the IGListKit infra. @param indexPath The index path of the cell in the UICollectionView. @param listAdapter The adapter managing the infra. */ - (void)didEndDisplayingItemAtIndexPath:(NSIndexPath *)indexPath forListAdapter:(IGListAdapter *)listAdapter; @end ================================================ FILE: Source/IGListKit/Internal/IGListWorkingRangeHandler.mm ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListWorkingRangeHandler.h" #import #import #if !__has_include() #import "IGListAssert.h" #else #import #endif #import "IGListAdapter.h" #import "IGListSectionController.h" struct _IGListWorkingRangeHandlerIndexPath { NSInteger section; NSInteger row; size_t hash; bool operator==(const _IGListWorkingRangeHandlerIndexPath &other) const { return (section == other.section && row == other.row); } }; struct _IGListWorkingRangeHandlerSectionControllerWrapper { IGListSectionController *sectionController; bool operator==(const _IGListWorkingRangeHandlerSectionControllerWrapper &other) const { return (sectionController == other.sectionController); } }; struct _IGListWorkingRangeHandlerIndexPathHash { size_t operator()(const _IGListWorkingRangeHandlerIndexPath& o) const { return (size_t)o.hash; } }; struct _IGListWorkingRangeHashID { size_t operator()(const _IGListWorkingRangeHandlerSectionControllerWrapper &o) const { return (size_t)[o.sectionController hash]; } }; typedef std::unordered_set<_IGListWorkingRangeHandlerSectionControllerWrapper, _IGListWorkingRangeHashID> _IGListWorkingRangeSectionControllerSet; typedef std::unordered_set<_IGListWorkingRangeHandlerIndexPath, _IGListWorkingRangeHandlerIndexPathHash> _IGListWorkingRangeIndexPathSet; @interface IGListWorkingRangeHandler () @property (nonatomic, assign, readonly) NSInteger workingRangeSize; @end @implementation IGListWorkingRangeHandler { _IGListWorkingRangeIndexPathSet _visibleSectionIndices; _IGListWorkingRangeSectionControllerSet _workingRangeSectionControllers; } - (instancetype)initWithWorkingRangeSize:(NSInteger)workingRangeSize { if (self = [super init]) { _workingRangeSize = workingRangeSize; } return self; } - (void)willDisplayItemAtIndexPath:(NSIndexPath *)indexPath forListAdapter:(IGListAdapter *)listAdapter { IGParameterAssert(indexPath != nil); IGParameterAssert(listAdapter != nil); _visibleSectionIndices.insert({ .section = indexPath.section, .row = indexPath.row, .hash = indexPath.hash }); [self _updateWorkingRangesWithListAdapter:listAdapter]; } - (void)didEndDisplayingItemAtIndexPath:(NSIndexPath *)indexPath forListAdapter:(IGListAdapter *)listAdapter { IGParameterAssert(indexPath != nil); IGParameterAssert(listAdapter != nil); _visibleSectionIndices.erase({ .section = indexPath.section, .row = indexPath.row, .hash = indexPath.hash }); [self _updateWorkingRangesWithListAdapter:listAdapter]; } #pragma mark - Working Ranges - (void)_updateWorkingRangesWithListAdapter:(IGListAdapter *)listAdapter { IGAssertMainThread(); // This method is optimized C++ to improve straight-line speed of these operations. Change at your peril. // We use a std::set because it is ordered. std::set visibleSectionSet = std::set(); for (const _IGListWorkingRangeHandlerIndexPath &indexPath : _visibleSectionIndices) { visibleSectionSet.insert(indexPath.section); } NSInteger start; NSInteger end; if (visibleSectionSet.size() == 0) { // We're now devoid of any visible sections. Bail start = 0; end = 0; } else { start = MAX(*visibleSectionSet.begin() - _workingRangeSize, 0); end = MIN(*visibleSectionSet.rbegin() + 1 + _workingRangeSize, (NSInteger)listAdapter.objects.count); } // Build the current set of working range section controllers _IGListWorkingRangeSectionControllerSet workingRangeSectionControllers (MAX(visibleSectionSet.size(), 1)); for (NSInteger idx = start; idx < end; idx++) { id item = [listAdapter objectAtSection:idx]; IGListSectionController *sectionController = [listAdapter sectionControllerForObject:item]; workingRangeSectionControllers.insert({sectionController}); } // Tell any new section controllers that they have entered the working range for (const _IGListWorkingRangeHandlerSectionControllerWrapper &wrapper : workingRangeSectionControllers) { // Check if the item exists in the old working range item array. auto it = _workingRangeSectionControllers.find(wrapper); if (it == _workingRangeSectionControllers.end()) { // The section controller isn't in the existing list, so it's new. id workingRangeDelegate = wrapper.sectionController.workingRangeDelegate; [workingRangeDelegate listAdapter:listAdapter sectionControllerWillEnterWorkingRange:wrapper.sectionController]; } } // Tell any removed section controllers that they have exited the working range for (const _IGListWorkingRangeHandlerSectionControllerWrapper &wrapper : _workingRangeSectionControllers) { // Check if the item exists in the new list of section controllers auto it = workingRangeSectionControllers.find(wrapper); if (it == workingRangeSectionControllers.end()) { // If the item does not exist in the new list, then it's been removed. id workingRangeDelegate = wrapper.sectionController.workingRangeDelegate; [workingRangeDelegate listAdapter:listAdapter sectionControllerDidExitWorkingRange:wrapper.sectionController]; } } _workingRangeSectionControllers = workingRangeSectionControllers; } @end ================================================ FILE: Source/IGListKit/Internal/UICollectionView+DebugDescription.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface UICollectionView (DebugDescription) - (NSArray *)debugDescriptionLines; @end ================================================ FILE: Source/IGListKit/Internal/UICollectionView+DebugDescription.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UICollectionView+DebugDescription.h" #if !__has_include() #import "IGListMacros.h" #else #import #endif @implementation UICollectionView (DebugDescription) - (NSArray *)debugDescriptionLines { NSMutableArray *debug = [NSMutableArray new]; #if defined(IGLK_DEBUG_DESCRIPTION_ENABLED) && IGLK_DEBUG_DESCRIPTION_ENABLED [debug addObject:[NSString stringWithFormat:@"Class: %@, instance: %p", NSStringFromClass(self.class), self]]; [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; [debug addObject:[NSString stringWithFormat:@"Delegate: %@", self.delegate]]; [debug addObject:[NSString stringWithFormat:@"Layout: %@", self.collectionViewLayout]]; [debug addObject:[NSString stringWithFormat:@"Frame: %@, bounds: %@", NSStringFromCGRect(self.frame), NSStringFromCGRect(self.bounds)]]; const NSInteger sections = [self numberOfSections]; [debug addObject:[NSString stringWithFormat:@"Number of sections: %lld", (long long)sections]]; for (NSInteger section = 0; section < sections; section++) { [debug addObject:[NSString stringWithFormat:@" %lld items in section %lld", (long long)[self numberOfItemsInSection:section], (long long)section]]; } [debug addObject:@"Visible cell details:"]; NSArray *visibleIndexPaths = [[self indexPathsForVisibleItems] sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *path in visibleIndexPaths) { [debug addObject:[NSString stringWithFormat:@" Visible cell at section %lld, item %lld:", (long long)path.section, (long long)path.item]]; [debug addObject:[NSString stringWithFormat:@" %@", [[self cellForItemAtIndexPath:path] description] ?: @""]]; } #endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED return debug; } @end ================================================ FILE: Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListBatchUpdateData; @interface UICollectionView (IGListBatchUpdateData) - (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData; @end ================================================ FILE: Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UICollectionView+IGListBatchUpdateData.h" #if !__has_include() #import "IGListBatchUpdateData.h" #else #import #endif @implementation UICollectionView (IGListBatchUpdateData) - (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData { [self deleteItemsAtIndexPaths:updateData.deleteIndexPaths]; [self insertItemsAtIndexPaths:updateData.insertIndexPaths]; [self reloadItemsAtIndexPaths:updateData.updateIndexPaths]; for (IGListMoveIndexPath *move in updateData.moveIndexPaths) { [self moveItemAtIndexPath:move.from toIndexPath:move.to]; } for (IGListMoveIndex *move in updateData.moveSections) { [self moveSection:move.from toSection:move.to]; } [self deleteSections:updateData.deleteSections]; [self insertSections:updateData.insertSections]; } @end ================================================ FILE: Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListAdapter.h" NS_ASSUME_NONNULL_BEGIN @interface UICollectionViewLayout (InteractiveReordering) // Should be called once to swizzle native APIs + (void)setupInteractiveReordering; - (void)ig_hijackLayoutInteractiveReorderingMethodForAdapter:(IGListAdapter *)adapter; - (nullable NSIndexPath *)updatedTargetForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath toIndexPath:(NSIndexPath *)originalTarget adapter:(IGListAdapter *)adapter; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UICollectionViewLayout+InteractiveReordering.h" #import #import "IGListAdapterInternal.h" #import "IGListSectionController.h" @implementation UICollectionViewLayout (InteractiveReordering) static void * kIGListAdapterKey = &kIGListAdapterKey; + (void)setupInteractiveReordering { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class layoutClass = [self class]; // override implementation for targetIndexPathForInteractivelyMovingItem:withPosition: SEL userMoveSelector = @selector(targetIndexPathForInteractivelyMovingItem:withPosition:); SEL overrideMoveSelector = @selector(ig_targetIndexPathForInteractivelyMovingItem:withPosition:); Method userMoveMethod = class_getInstanceMethod(layoutClass, userMoveSelector); Method overrideMoveMethod = class_getInstanceMethod(layoutClass, overrideMoveSelector); IMP userMoveIMP = method_getImplementation(userMoveMethod); IMP overrideMoveIMP = method_getImplementation(overrideMoveMethod); class_replaceMethod(layoutClass, overrideMoveSelector, userMoveIMP, method_getTypeEncoding(userMoveMethod)); class_replaceMethod(layoutClass, userMoveSelector, overrideMoveIMP, method_getTypeEncoding(overrideMoveMethod)); // override implementation for // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: SEL userInvalidationSelector = @selector(invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition:); SEL overrideInvalidationSelector = @selector(ig_invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition:); Method userInvalidationMethod = class_getInstanceMethod(layoutClass, userInvalidationSelector); Method overrideInvalidationMethod = class_getInstanceMethod(layoutClass, overrideInvalidationSelector); IMP userInvalidationIMP = method_getImplementation(userInvalidationMethod); IMP overrideInvalidationIMP = method_getImplementation(overrideInvalidationMethod); class_replaceMethod(layoutClass, overrideInvalidationSelector, userInvalidationIMP, method_getTypeEncoding(userInvalidationMethod)); class_replaceMethod(layoutClass, userInvalidationSelector, overrideInvalidationIMP, method_getTypeEncoding(overrideInvalidationMethod)); // override implementation for // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: SEL userEndInvalidationSelector = @selector(invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled:); SEL overrideEndInvalidationSelector = @selector(ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled:); Method userEndInvalidationMethod = class_getInstanceMethod(layoutClass, userEndInvalidationSelector); Method overrideEndInvalidationMethod = class_getInstanceMethod(layoutClass, overrideEndInvalidationSelector); IMP userEndInvalidationIMP = method_getImplementation(userEndInvalidationMethod); IMP overrideEndInvalidationIMP = method_getImplementation(overrideEndInvalidationMethod); class_replaceMethod(layoutClass, overrideEndInvalidationSelector, userEndInvalidationIMP, method_getTypeEncoding(userEndInvalidationMethod)); class_replaceMethod(layoutClass, userEndInvalidationSelector, overrideEndInvalidationIMP, method_getTypeEncoding(overrideEndInvalidationMethod)); }); } - (void)ig_hijackLayoutInteractiveReorderingMethodForAdapter:(IGListAdapter *)adapter { objc_setAssociatedObject(self, kIGListAdapterKey, adapter, OBJC_ASSOCIATION_ASSIGN); } - (NSIndexPath *)ig_targetIndexPathForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath withPosition:(CGPoint)position NS_AVAILABLE_IOS(9_0) { // call looks recursive, but through swizzling is calling the original implementation for // targetIndexPathForInteractivelyMovingItem:withPosition: NSIndexPath *originalTarget = [self ig_targetIndexPathForInteractivelyMovingItem:previousIndexPath withPosition:position]; IGListAdapter *adapter = (IGListAdapter *)objc_getAssociatedObject(self, kIGListAdapterKey); if (adapter == nil) { return originalTarget; } NSIndexPath *updatedTarget = [self updatedTargetForInteractivelyMovingItem:previousIndexPath toIndexPath:originalTarget adapter:adapter]; return updatedTarget ?: originalTarget; } - (nullable NSIndexPath *)updatedTargetForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath toIndexPath:(NSIndexPath *)originalTarget adapter:(IGListAdapter *)adapter { const NSInteger sourceSectionIndex = previousIndexPath.section; NSInteger destinationSectionIndex = originalTarget.section; NSInteger destinationItemIndex = originalTarget.item; IGListSectionController *sourceSectionController = [adapter sectionControllerForSection:sourceSectionIndex]; IGListSectionController *destinationSectionController = [adapter sectionControllerForSection:destinationSectionIndex]; adapter.isLastInteractiveMoveToLastSectionIndex = NO; // this is a reordering of sections themselves if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1) { if (destinationItemIndex == 1) { // the "item" representing our section was dropped // into the end of a destination section rather than the beginning // so it really belongs one section after the section where it landed if (destinationSectionIndex < ((NSInteger)[[adapter objects] count] - 1)) { destinationSectionIndex += 1; destinationItemIndex = 0; } else { // if we're moving an item to the last spot, our index would exceed the number of sections available // so we have to special case this scenario. iOS doesnt allow an item move to "create" a new section adapter.isLastInteractiveMoveToLastSectionIndex = YES; } NSIndexPath *updatedTarget = [NSIndexPath indexPathForItem:destinationItemIndex inSection:destinationSectionIndex]; return updatedTarget; } } return nil; } - (UICollectionViewLayoutInvalidationContext *)ig_invalidationContextForInteractivelyMovingItems:(NSArray *)targetIndexPaths withTargetPosition:(CGPoint)targetPosition previousIndexPaths:(NSArray *)previousIndexPaths previousPosition:(CGPoint)previousPosition { // call looks recursive, but through swizzling is calling the original implementation for // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: UICollectionViewLayoutInvalidationContext *originalContext = [self ig_invalidationContextForInteractivelyMovingItems:targetIndexPaths withTargetPosition:targetPosition previousIndexPaths:previousIndexPaths previousPosition:previousPosition]; return [self ig_cleanupInvalidationContext:originalContext]; } - (UICollectionViewLayoutInvalidationContext *)ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:(NSArray *)indexPaths previousIndexPaths:(NSArray *)previousIndexPaths movementCancelled:(BOOL)movementCancelled { // call looks recursive, but through swizzling is calling the original implementation for // invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled: UICollectionViewLayoutInvalidationContext *originalContext = [self ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:indexPaths previousIndexPaths:previousIndexPaths movementCancelled:movementCancelled]; return [self ig_cleanupInvalidationContext:originalContext]; } - (UICollectionViewLayoutInvalidationContext *)ig_cleanupInvalidationContext:(UICollectionViewLayoutInvalidationContext *)originalContext { IGListAdapter *adapter = (IGListAdapter *)objc_getAssociatedObject(self, kIGListAdapterKey); if (adapter == nil || !self.collectionView) { return originalContext; } const NSInteger numSections = [adapter numberOfSectionsInCollectionView:(UICollectionView * _Nonnull)self.collectionView]; // protect against invalidating an index path that no longer exists // (like item 1 in the last section after interactively reordering an item to the end of a list of 1 item sections) if ([originalContext.invalidatedItemIndexPaths count] > 0) { NSUInteger indexToRemove = NSNotFound; indexToRemove = [originalContext.invalidatedItemIndexPaths indexOfObjectPassingTest: ^BOOL(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if (obj.section == numSections-1) { IGListSectionController *section = [adapter sectionControllerForSection:obj.section]; return obj.item > [section numberOfItems] - 1; } return NO; }]; if (indexToRemove != NSNotFound) { NSMutableArray *invalidatedItemIndexPaths = [originalContext.invalidatedItemIndexPaths mutableCopy]; [invalidatedItemIndexPaths removeObjectAtIndex:indexToRemove]; UICollectionViewLayoutInvalidationContext *modifiedContext; if ([originalContext isKindOfClass:[UICollectionViewFlowLayoutInvalidationContext class]]) { // UICollectionViewFlowLayout has a special invalidation context subclass UICollectionViewFlowLayoutInvalidationContext *flowModifiedContext = [[self.class invalidationContextClass] new]; flowModifiedContext.invalidateFlowLayoutDelegateMetrics = [(UICollectionViewFlowLayoutInvalidationContext *)originalContext invalidateFlowLayoutDelegateMetrics]; flowModifiedContext.invalidateFlowLayoutAttributes = [(UICollectionViewFlowLayoutInvalidationContext *)originalContext invalidateFlowLayoutAttributes]; modifiedContext = flowModifiedContext; } else { modifiedContext = [[self.class invalidationContextClass] new]; } [modifiedContext invalidateItemsAtIndexPaths:invalidatedItemIndexPaths]; [self ig_invalidateAccessoryElementsWithSupplementaryIndexPaths:originalContext.invalidatedSupplementaryIndexPaths decorationIndexPaths:originalContext.invalidatedDecorationIndexPaths inContext:modifiedContext]; modifiedContext.contentOffsetAdjustment = originalContext.contentOffsetAdjustment; modifiedContext.contentSizeAdjustment = originalContext.contentSizeAdjustment; return modifiedContext; } } return originalContext; } - (void)ig_invalidateAccessoryElementsWithSupplementaryIndexPaths:(NSDictionary *> *)supplementaryIndexPaths decorationIndexPaths:(NSDictionary *> *)decorationIndexPaths inContext:(UICollectionViewLayoutInvalidationContext *)context { [supplementaryIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { [context invalidateSupplementaryElementsOfKind:key atIndexPaths:obj]; }]; [decorationIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *obj, BOOL *stop) { [context invalidateDecorationElementsOfKind:key atIndexPaths:obj]; }]; } @end ================================================ FILE: Source/IGListKit/Internal/UIScrollView+IGListKit.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface UIScrollView (IGListKit) - (UIEdgeInsets) ig_contentInset; @end ================================================ FILE: Source/IGListKit/Internal/UIScrollView+IGListKit.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UIScrollView+IGListKit.h" @implementation UIScrollView (IGListKit) - (UIEdgeInsets) ig_contentInset { UIEdgeInsets contentInset = self.contentInset; if (@available(iOS 11.0, tvOS 11.0, *)) { contentInset = self.adjustedContentInset; } return contentInset; } @end ================================================ FILE: Source/IGListKit/Internal/UIViewController+IGListAdapterInternal.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UIViewController+IGListAdapter.h" NS_ASSUME_NONNULL_BEGIN @interface UIViewController (IGListAdapterInternal) - (void)associateListAdapter:(IGListAdapter *)adapter; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/UIViewController+IGListAdapter.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGListAdapter; NS_ASSUME_NONNULL_BEGIN @interface UIViewController (IGListAdapter) /// Adapters that have this VC as their `viewController` - (NSArray *)associatedListAdapters; @end NS_ASSUME_NONNULL_END ================================================ FILE: Source/IGListKit/UIViewController+IGListAdapter.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "UIViewController+IGListAdapter.h" #import @implementation UIViewController (IGListAdapter) - (NSArray *)associatedListAdapters { return [[self _associatedListAdaptersTable] allObjects]; } - (void)associateListAdapter:(IGListAdapter *)adapter { NSHashTable *const table = [self _associatedListAdaptersTable]; [table addObject:adapter]; } - (NSHashTable *)_associatedListAdaptersTable { NSHashTable *table = objc_getAssociatedObject(self, @selector(_associatedListAdaptersTable)); if (table) { return table; } table = [NSHashTable weakObjectsHashTable]; objc_setAssociatedObject(self, @selector(_associatedListAdaptersTable), table, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return table; } @end ================================================ FILE: Source/IGListSwiftKit/IGListAdapter+Async.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit public extension ListAdapter { /// Perform an update from the previous state of the data source. This is analogous to calling /// `UICollectionView.performBatchUpdates(_:completion:)`. /// /// - Parameter animated: A flag indicating if the transition should be animated. /// - Returns: `true` if the update animations completed successfully; otherwise `false`. @discardableResult @available(iOS 13.0, *) @available(tvOS 13.0, *) func performUpdates(animated: Bool) async -> Bool { return await withCheckedContinuation { continuation in performUpdates(animated: animated) { finished in continuation.resume(returning: finished) } } } /// Perform an immediate reload of the data in the data source, discarding the old objects. /// /// - Returns: `true` if the update animations completed successfully; otherwise `false`. /// /// @warning Do not use this method to update without animations as it can be very expensive to teardown and rebuild all /// section controllers. Use `performUpdates(animated:) async` instead. @discardableResult @available(iOS 13.0, *) @available(tvOS 13.0, *) func reloadData() async -> Bool { return await withCheckedContinuation { continuation in reloadData { finished in continuation.resume(returning: finished) } } } } ================================================ FILE: Source/IGListSwiftKit/IGListCollectionContext+Refinements.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import UIKit extension ListCollectionContext { /** Dequeues a cell from the collection view reuse pool. - Parameters: - reuseIdentifier: A reuse identifier for the specified cell. This parameter may be `nil`. - sectionController: The section controller requesting this information. - index: The index of the cell. - Returns: A cell dequeued from the reuse pool or a newly created one. */ public func dequeueReusableCell( withReuseIdentifier reuseIdentifier: String, for sectionController: ListSectionController, at index: Int ) -> T { guard let cell = self.dequeueReusableCell( of: T.self, withReuseIdentifier: reuseIdentifier, for: sectionController, at: index ) as? T else { fatalError() } return cell } /** Dequeues a cell from the collection view reuse pool. - Parameters: - sectionController: The section controller requesting this information. - index: The index of the cell. - Returns: A cell dequeued from the reuse pool or a newly created one. - Note: This method uses a string representation of the cell class as the identifier. */ public func dequeueReusableCell( for sectionController: ListSectionController, at index: Int ) -> T { guard let cell = self.dequeueReusableCell( of: T.self, for: sectionController, at: index ) as? T else { fatalError() } return cell } /** Dequeues a cell from the collection view reuse pool. - Parameters: - nibName: The name of the nib file. - bundle: The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. - sectionController: The section controller requesting this information. - index: The index of the cell. - Returns: A cell dequeued from the reuse pool or a newly created one. - Note: This method uses the nib name as the reuse identifier. */ public func dequeueReusableCell( withNibName nibName: String, bundle: Bundle?, for sectionController: ListSectionController, at index: Int ) -> T { guard let cell = self.dequeueReusableCell( withNibName: nibName, bundle: bundle, for: sectionController, at: index ) as? T else { fatalError("A nib named \"\(nibName)\" was not found in \(String(describing:bundle))") } return cell } /** Dequeues a storyboard prototype cell from the collection view reuse pool. - Parameters: - identifier: The identifier of the cell prototype in storyboard. - sectionController: The section controller requesting this information. - index: The index of the cell. - Returns: A cell dequeued from the reuse pool or a newly created one. */ public func dequeueReusableCellFromStoryboard( withIdentifier reuseIdentifier: String, for sectionController: ListSectionController, at index: Int ) -> T { guard let cell = self.dequeueReusableCellFromStoryboard( withIdentifier: reuseIdentifier, for: sectionController, at: index ) as? T else { fatalError("A cell with the identifier \"\(reuseIdentifier)\" was not found in the storyboard") } return cell } /** Dequeues a supplementary view from the collection view reuse pool. - Parameters: - elementKind: The kind of supplementary view. - sectionController: The section controller requesting this information. - index: The index of the supplementary view. - Returns: A supplementary view dequeued from the reuse pool or a newly created one. - Note: This method uses a string representation of the view class and the kind as the identifier. */ public func dequeueReusableSupplementaryView( ofKind elementKind: String, forSectionController sectionController: ListSectionController, atIndex index: NSInteger ) -> T { guard let supplementaryView = self.dequeueReusableSupplementaryView( ofKind: elementKind, for: sectionController, class: T.self, at: index ) as? T else { fatalError() } return supplementaryView } /** Dequeues a supplementary view from the collection view reuse pool. - Parameters: - elementKind: The kind of supplementary view. - identifier: The identifier of the supplementary view in storyboard. - sectionController: The section controller requesting this information. - index: The index of the supplementary view. - Returns: A supplementary view dequeued from the reuse pool or a newly created one. */ public func dequeueReusableSupplementaryViewFromStoryboard( ofKind elementKind: String, withIdentifier identifier: String, forSectionController sectionController: ListSectionController, atIndex index: Int ) -> T { guard let supplementaryView = self.dequeueReusableSupplementaryView( fromStoryboardOfKind: elementKind, withIdentifier: identifier, for: sectionController, at: index ) as? T else { fatalError() } return supplementaryView } /** Dequeues a supplementary view from the collection view reuse pool. - Parameters: - elementKind: The kind of supplementary view. - sectionController: The section controller requesting this information. - nibName: The name of the nib file. - bundle: The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. - index: The index of the supplementary view. - Returns: A supplementary view dequeued from the reuse pool or a newly created one. - Note: This method uses the nib name as the reuse identifier. */ public func dequeueReusableSupplementaryView( ofKind elementKind: String, forSectionController sectionController: ListSectionController, nibName: String, bundle: Bundle?, atIndex index: Int ) -> T { guard let supplementaryView = self.dequeueReusableSupplementaryView( ofKind: elementKind, for: sectionController, nibName: nibName, bundle: bundle, at: index ) as? T else { fatalError() } return supplementaryView } } ================================================ FILE: Source/IGListSwiftKit/IGListSingleSectionController+Refinements.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit import Foundation extension ListSingleSectionController { /** Creates a new section controller for a given cell type that will always have only one cell when present in a list. - Parameters: - configure: A closure that configures the cell with the item given to the section controller. - size: A closure that returns the size for the cell given the collection context. - Returns: A new section controller. @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually `self`) or the `ListAdapter`. Pass in locally scoped objects or use `weak` references! */ public convenience init( configure: @escaping (Item, Cell) -> Void, size: @escaping (Item, ListCollectionContext?) -> CGSize ) { self.init( cellClass: Cell.self, configureBlock: { configure($0 as! Item, $1 as! Cell) }, sizeBlock: { size($0 as! Item, $1) } ) } /** Creates a new section controller for a given cell type that will always have only one cell when present in a list. Supports any Swift value conforming to `ListIdentifiable`. - Parameters: - configure: A closure that configures the cell with the Swift value item given to the section controller. - size: A closure that returns the size for the cell given the collection context. - Returns: A new section controller. @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually `self`) or the `ListAdapter`. Pass in locally scoped objects or use `weak` references! */ public convenience init( configure: @escaping (Value, Cell) -> Void, size: @escaping (Value, ListCollectionContext?) -> CGSize ) { self.init( cellClass: Cell.self, configureBlock: { (item: Any, cell: UICollectionViewCell) in guard let value = Value(diffable: item) else { fatalError("Expected object for value section controller to be a boxed \(Value.self), but it was \(item)") } configure(value, cell as! Cell) }, sizeBlock: { (item: Any, context: ListCollectionContext?) in guard let value = Value(diffable: item) else { fatalError("Expected object for value section controller to be a boxed \(Value.self), but it was \(item)") } return size(value, context) } ) } } ================================================ FILE: Source/IGListSwiftKit/IGListSwiftKit.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import //! Project version number for IGListSwiftKit. FOUNDATION_EXPORT double IGListSwiftKitVersionNumber; //! Project version string for IGListSwiftKit. FOUNDATION_EXPORT const unsigned char IGListSwiftKitVersionString[]; ================================================ FILE: Source/IGListSwiftKit/ListIdentifiable.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListDiffKit /// The `ListIdentifiable` protocol is a subset of `ListDiffable`'s functionality, /// for use with Swift value types and `ListValueSectionController`. /// /// `ListIdentifiable` is an experimental, under-development API, and may change without warning in the future. public protocol ListIdentifiable: Equatable { var diffIdentifier: NSObjectProtocol { get } } public extension ListIdentifiable { /// Provides an object version of the value that can be passed to Objective-C APIs from /// `IGListKit` that require objects. /// /// The object class is a private implementation detail of `IGListSwiftKit`. Use of this /// API must be paired with the `ListValueSectionController` class, which unwraps the /// value for its subclasses. func diffable() -> ListDiffable { return ListDiffableValueBox(value: self) } /// Determines whether an arbitrary `Any` value is an object version of the identifiable value. static func isDiffable(_ value: Any) -> Bool { return value is ListDiffableValueBox } // TODO(natesm): Should this be a public API? It is for now. init?(diffable: Any) { guard let value = (diffable as? ListDiffableValueBox)?.value else { return nil } self = value } } public extension Sequence where Element: ListIdentifiable { func diffables() -> [ListDiffable] { return map { $0.diffable() } } } /// An internal class for boxing Swift values, for use with the `ListValueSectionController` class. /// /// The public boxing API is provided by a protocol extension of `ListIdentifiable`. private final class ListDiffableValueBox: NSObject, ListDiffable { let value: Value init(value: Value) { self.value = value } // MARK: - ListDiffable func diffIdentifier() -> NSObjectProtocol { return value.diffIdentifier } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let other = object as? ListDiffableValueBox else { return false } return value == other.value } } ================================================ FILE: Source/IGListSwiftKit/ListValueSectionController.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import IGListKit /// A generic section controller that supports any Swift value conforming to `ListIdentifiable`. /// /// For a type-safe base class for `ListDiffable` objects, see `ListGenericSectionController` instead. /// /// `ListValueSectionController` is an experimental, under-development API, and may change without warning in the future. open class ListValueSectionController: ListSectionController { /// The section controller's current value. /// /// This value will be `nil` temporarily after initialization, but is exposed as an implicitly-unwrapped /// optional because it will be set by `didUpdate(to:)` before any other methods, like `cellForItem(at:)`, /// are invoked. public private(set) var value: Value! /// Subclasses of `ListValueSectionController` may not override `didUpdate(to:)` for objects. /// Instead, they must use the overload for generic values. public final override func didUpdate(to object: Any) { guard let value = Value(diffable: object) else { fatalError("Expected object for value section controller to be a boxed \(Value.self), but it was \(object)") } self.value = value didUpdate(to: value) } /// Updates the section controller to a new value. /// /// Subclasses of `ListValueSectionController` may override this method. Calling `super` is not required. open func didUpdate(to value: Value) {} } ================================================ FILE: Source/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 5.2.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Tests/Assets/IGTestNibCell.xib ================================================ ================================================ FILE: Tests/Assets/IGTestNibSupplementaryView.xib ================================================ ================================================ FILE: Tests/Assets/IGTestStoryboard.storyboard ================================================ ================================================ FILE: Tests/IGListAdapterDelegateAnnouncerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterInternal.h" #import "IGListAdapterUpdater.h" #import "IGListTestHelpers.h" #import "IGTestDelegateDataSource.h" #import "IGTestObject.h" #import "IGListAdapterDelegateAnnouncer.h" #import "IGTestCell.h" @interface IGListAdapterDelegateAnnouncerTests : XCTestCase // These objects are created for you in -setUp @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UIViewController *viewController; @property (nonatomic, strong) IGListAdapterDelegateAnnouncer *announcer; @property (nonatomic, strong) UICollectionView *collectionView1; @property (nonatomic, strong) UICollectionView *collectionView2; @property (nonatomic, strong) id dataSource1; @property (nonatomic, strong) id dataSource2; @property (nonatomic, strong) IGListAdapter *adapter1; @property (nonatomic, strong) IGListAdapter *adapter2; @end @implementation IGListAdapterDelegateAnnouncerTests - (void)setUp { [super setUp]; self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; self.viewController = [UIViewController new]; self.announcer = [IGListAdapterDelegateAnnouncer new]; self.collectionView1 = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:self.collectionView1]; self.collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:self.collectionView2]; IGTestDelegateDataSource *const dataSource1 = [IGTestDelegateDataSource new]; self.dataSource1 = dataSource1; IGTestDelegateDataSource *const dataSource2 = [IGTestDelegateDataSource new]; self.dataSource2 = dataSource2; self.adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:self.viewController]; self.adapter1.globalDelegateAnnouncer = self.announcer; self.adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:self.viewController]; self.adapter2.globalDelegateAnnouncer = self.announcer; } - (void)setupAdapter1WithObjects:(NSArray *)objects { self.dataSource1.objects = objects; self.adapter1.collectionView = self.collectionView1; self.adapter1.dataSource = self.dataSource1; [self.collectionView1 layoutIfNeeded]; } - (void)setupAdapter2WithObjects:(NSArray *)objects { self.dataSource2.objects = objects; self.adapter2.collectionView = self.collectionView2; self.adapter2.dataSource = self.dataSource2; [self.collectionView2 layoutIfNeeded]; } #pragma mark - Single adapter, multiple listeners - (void)test_whenShowingOneItem_withTwoListeners_withOneAdapter_thatBothListenersReceivesWillDisplay{ [self setupAdapter1WithObjects:@[]]; IGTestObject *const object = genTestObject(@1, @1); self.dataSource1.objects = @[ object ]; NSIndexPath *const indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; id mockDisplayHandler1 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler1]; [[mockDisplayHandler1 expect] listAdapter:self.adapter1 willDisplayObject:object atIndex:0]; [[mockDisplayHandler1 expect] listAdapter:self.adapter1 willDisplayObject:object cell: [OCMArg any] atIndexPath:indexPath]; id mockDisplayHandler2 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler2]; [[mockDisplayHandler2 expect] listAdapter:self.adapter1 willDisplayObject:object atIndex:0]; [[mockDisplayHandler2 expect] listAdapter:self.adapter1 willDisplayObject:object cell: [OCMArg any] atIndexPath:indexPath]; XCTestExpectation *expectation = genExpectation; [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished2) { [mockDisplayHandler1 verify]; [mockDisplayHandler2 verify]; XCTAssertTrue(finished2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenRemovignOneItem_withTwoListeners_withOneAdapter_thatBothListenersReceivesEndDisplay { IGTestObject *const object = genTestObject(@1, @1); NSIndexPath *const zeroIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self setupAdapter1WithObjects:@[object]]; self.dataSource1.objects = @[]; id mockDisplayHandler1 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler1]; [[mockDisplayHandler1 expect] listAdapter:self.adapter1 didEndDisplayingObject:object atIndex:0]; [[mockDisplayHandler1 expect] listAdapter:self.adapter1 didEndDisplayingObject:object cell:[OCMArg any] atIndexPath: zeroIndexPath]; id mockDisplayHandler2 = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler2]; [[mockDisplayHandler2 expect] listAdapter:self.adapter1 didEndDisplayingObject:object atIndex:0]; [[mockDisplayHandler2 expect] listAdapter:self.adapter1 didEndDisplayingObject:object cell:[OCMArg any] atIndexPath: zeroIndexPath]; XCTestExpectation *expectation = genExpectation; [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished2) { [mockDisplayHandler1 verify]; [mockDisplayHandler2 verify]; XCTAssertTrue(finished2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Remove listener - (void)test_whenRemovingListener_thatListenerDoesNotReceiveCallbacks { [self setupAdapter1WithObjects:@[]]; IGTestObject *const object = genTestObject(@1, @1); self.dataSource1.objects = @[object]; id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler]; [self.announcer removeListener:mockDisplayHandler]; // Listener was removed, so it should NOT receive any callbacks [[mockDisplayHandler reject] listAdapter:[OCMArg any] willDisplayObject:[OCMArg any] atIndex:0]; [[mockDisplayHandler reject] listAdapter:[OCMArg any] willDisplayObject:[OCMArg any] cell:[OCMArg any] atIndexPath:[OCMArg any]]; XCTestExpectation *expectation = genExpectation; [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished) { [mockDisplayHandler verify]; XCTAssertTrue(finished); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Two adapters, single listener - (void)test_whenShowingTwoItems_withOneListeners_withTwoAdapters_thatBothItemsSendWillDisplay { [self setupAdapter1WithObjects:@[]]; [self setupAdapter2WithObjects:@[]]; IGTestObject *const object1 = genTestObject(@1, @1); NSIndexPath *const zeroIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; self.dataSource1.objects = @[ object1 ]; IGTestObject *const object2 = genTestObject(@1, @1); self.dataSource2.objects = @[ object2 ]; id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; [self.announcer addListener:mockDisplayHandler]; [[mockDisplayHandler expect] listAdapter:self.adapter1 willDisplayObject:object1 atIndex:0]; [[mockDisplayHandler expect] listAdapter:self.adapter1 willDisplayObject:object1 cell: [OCMArg any] atIndexPath: zeroIndexPath]; [[mockDisplayHandler expect] listAdapter:self.adapter2 willDisplayObject:object2 atIndex:0]; [[mockDisplayHandler expect] listAdapter:self.adapter2 willDisplayObject:object2 cell: [OCMArg any] atIndexPath: zeroIndexPath]; XCTestExpectation *expectation = genExpectation; [self.adapter1 performUpdatesAnimated:NO completion:^(BOOL finished1) { [self.adapter2 performUpdatesAnimated:NO completion:^(BOOL finished2) { [mockDisplayHandler verify]; XCTAssertTrue(finished1); XCTAssertTrue(finished2); [expectation fulfill]; }]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } @end ================================================ FILE: Tests/IGListAdapterE2ETests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterInternal.h" #import "IGListAdapterUpdateTester.h" #import "IGListAdapterUpdater.h" #import "IGListAdapterUpdaterInternal.h" #import "IGListTestCase.h" #import "IGListTestCollectionViewLayout.h" #import "IGListTestHelpers.h" #import "IGListTestOffsettingLayout.h" #import "IGListUpdateTransactionBuilder.h" #import "IGTestCell.h" #import "IGTestDelegateController.h" #import "IGTestDelegateDataSource.h" #import "IGTestObject.h" @interface IGListAdapterE2ETests : IGListTestCase @end @implementation IGListAdapterE2ETests - (void)setUp { self.workingRangeSize = 2; self.dataSource = [IGTestDelegateDataSource new]; [super setUp]; } - (void)test_whenSettingUpTest_thenCollectionViewIsLoaded { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @3) ]]; XCTAssertEqual(self.collectionView.numberOfSections, 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); } - (void)test_whenUsingStringValue_thenCellLabelsAreConfigured { [self setupWithObjects:@[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]]; IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; XCTAssertEqualObjects(cell.label.text, @"Foo"); XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]); } - (void)test_whenUpdating_withEqualObjects_thatCellConfigurationDoesntChange { [self setupWithObjects:@[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]]; // Get the section controller before we change the data source or perform updates id c0 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; // Set equal but new-instance objects on the data source self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // Perform updates on the adapter and check that the cell config uses the same section controller as before the updates XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestCell *cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; XCTAssertEqualObjects(cell.label.text, @"Foo"); XCTAssertNotNil(cell.delegate); XCTAssertEqual(cell.delegate, c0); XCTAssertEqual(cell.delegate, [self.adapter sectionControllerForObject:self.dataSource.objects[0]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingItem_cellConfigurationChanges { [self setupWithObjects:@[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]]; // make sure our cells are propertly configured IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); // Change the string value of both instances in the data source IGTestObject *item1 = self.dataSource.objects[0]; item1.value = @"Baz"; IGTestObject *item2 = self.dataSource.objects[1]; item2.value = @"Quz"; // Only reload the first item, not the second [self.adapter reloadObjects:@[item1]]; // The collection view will likely create new cells cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(1, 0)]; // Make sure that the cell in the first section was reloaded XCTAssertEqualObjects(cell1.label.text, @"Baz"); // The cell in the second section should not be reloaded and should equal the string value from setup XCTAssertEqualObjects(cell2.label.text, @"Bar"); } - (void)test_whenObjectEqualityChanges_thatSectionCountChanges { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; self.dataSource.objects = @[ genTestObject(@1, @2), genTestObject(@2, @3), // updated to 3 items (from 2) genTestObject(@3, @2), // insert new object ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual(self.collectionView.numberOfSections, 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdatesComplete_thatCellsExist { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), ]]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadDataCompletes_thatCellsExist { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; XCTestExpectation *expectation = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:1]]); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerInsertsIndexes_thatCountsAreUpdated { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerDeletesIndexes_thatCountsAreUpdated { // 2 sections each with 2 objects [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @1; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerReloadsIndexes_thatCellConfigurationUpdates { [self setupWithObjects:@[ genTestObject(@1, @"a"), genTestObject(@2, @"b") ]]; XCTAssertEqual([self.collectionView numberOfSections], 2); IGTestCell *cell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqualObjects(cell.label.text, @"a"); IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @"c"; [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); IGTestCell *updatedCell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqualObjects(updatedCell.label.text, @"c"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerReloads_thatCountsAreUpdated { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext reloadSectionController:sectionController]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerReloads_withPreferItemReload_thatCountsAreUpdated { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; // Prefer to use item reloads for section reloads if available. [(IGListAdapterUpdater *)self.adapter.updater setPreferItemReloadsForSectionReloads:YES]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext reloadSectionController:sectionController]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withSectionControllerMutations_thatCollectionCountsAreUpdated { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object1 = self.dataSource.objects[0]; IGTestObject *object2 = self.dataSource.objects[1]; // insert a new object in front of the one we are doing an item-level insert on self.dataSource.objects = @[ genTestObject(@3, @1), // new object1, object2, ]; IGListSectionController *sectionController1 = [self.adapter sectionControllerForObject:object1]; IGListSectionController *sectionController2 = [self.adapter sectionControllerForObject:object2]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTestExpectation *expectation = genExpectation; [sectionController1.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object1.value = @1; object2.value = @3; [batchContext deleteInSectionController:sectionController1 atIndexes:[NSIndexSet indexSetWithIndex:0]]; [batchContext insertInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:2]]; [batchContext reloadInSectionController:sectionController2 atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { // 3 sections now b/c of the insert XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerMoves_withSectionControllerMutations_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; IGTestObject *object = self.dataSource.objects[0]; self.dataSource.objects = @[ genTestObject(@2, @2), object, // moved from 0 to 1 ]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; // queue the update that performs the section move [self.adapter performUpdatesAnimated:YES completion:nil]; XCTestExpectation *expectation = genExpectation; // queue an item update that gets batched with the section move [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); // the object we are tracking should now be in section 1 and have 3 items XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenItemIsRemoved_withSectionControllerMutations_thatCollectionViewWorks { // 2 sections each with 2 objects [self setupWithObjects:@[ genTestObject(@2, @2), genTestObject(@1, @2) ]]; IGTestObject *object = self.dataSource.objects[1]; // object at index 1 deleted self.dataSource.objects = @[ genTestObject(@2, @2), ]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @1; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withUnequalItem_withItemMoving_thatCollectionViewCountsUpdate { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), ]]; self.dataSource.objects = @[ genTestObject(@3, @2), genTestObject(@1, @3), // moved from index 0 to 1, value changed from 2 to 3 genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withItemMoving_withSectionControllerReloadIndexes_thatCollectionViewCountsUpdate { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @3), ]]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; self.dataSource.objects = @[ genTestObject(@2, @3), genTestObject(@1, @2), // moved from index 0 to 1 ]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withSectionControllerReloadIndexes_withItemDeleted_thatCollectionViewCountsUpdate { [self setupWithObjects:@[ genTestObject(@1, @2), // item that will be deleted genTestObject(@2, @3), ]]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; self.dataSource.objects = @[ genTestObject(@2, @3), ]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withNewItemInstances_thatSectionControllersEqual { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2) ]]; // grab section controllers before updating the objects NSArray *beforeupdateObjects = self.dataSource.objects; IGListSectionController *sectionController1 = [self.adapter sectionControllerForObject:beforeupdateObjects.firstObject]; IGListSectionController *sectionController2 = [self.adapter sectionControllerForObject:beforeupdateObjects.lastObject]; self.dataSource.objects = @[ genTestObject(@1, @3), // new instance, value changed from 2 to 3 genTestObject(@2, @2), // new instance but unchanged ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); NSArray *afterupdateObjects = [self.adapter objects]; // pointer equality XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.firstObject], sectionController1); XCTAssertEqual([self.adapter sectionControllerForObject:afterupdateObjects.lastObject], sectionController2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingMultipleUpdates_withNewItemInstances_thatSectionControllersReceiveNewInstances { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), ]]; id object = self.dataSource.objects[0]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; // test delegate controller counts the number of times it receives -didUpdateToItem: XCTAssertEqual(sectionController.updateCount, 1); self.dataSource.objects = @[ object, // same object instance genTestObject(@3, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]); // should not have received -didUpdateToItem: since the instance did not change XCTAssertEqual(sectionController.updateCount, 1); self.dataSource.objects = @[ genTestObject(@1, @2), // new instance but equal genTestObject(@3, @2), ]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) { XCTAssertEqual(sectionController, [self.adapter sectionControllerForObject:[self.adapter objects][0]]); // a new instance was used, make sure the section controller was updated XCTAssertEqual(sectionController.updateCount, 2); [expectation fulfill]; }]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenQueryingCollectionContext_withNewItemInstances_thatSectionMatchesCurrentIndex { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), ]]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { self.dataSource.objects = @[ genTestObject(@2, @2), genTestObject(@1, @2), // new instance but equal genTestObject(@3, @2), ]; __block BOOL executedUpdateBlock = NO; __weak __typeof__(sectionController) weakSectionController = sectionController; sectionController.itemUpdateBlock = ^{ executedUpdateBlock = YES; XCTAssertEqual(weakSectionController.section, 1); }; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished3) { XCTAssertTrue(executedUpdateBlock); [expectation fulfill]; }]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerMutates_withReloadData_thatSectionControllerMutationIsApplied { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:2]]; } completion:nil]; XCTestExpectation *expectation = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); // check that the count of items in section 0 was updated from the previous batch update block XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #if !TARGET_OS_TV - (void)test_whenContentOffsetChanges_withPerformUpdates_thatCollectionViewWorks { // this test layout changes the offset in -prepareLayout which occurs somewhere between the update block being // applied and the completion block self.collectionView.collectionViewLayout = [IGListTestOffsettingLayout new]; [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), ]]; // remove the last object to check that we don't access OOB section controller when the layout changes the offset self.dataSource.objects = @[ genTestObject(@1, @2), genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #endif - (void)test_whenReloadingItems_withNewItemInstances_thatSectionControllersReceiveNewInstances { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), ]]; IGTestDelegateController *sectionController1 = [self.adapter sectionControllerForObject:genTestObject(@1, @2)]; IGTestDelegateController *sectionController2 = [self.adapter sectionControllerForObject:genTestObject(@2, @2)]; NSArray *newObjects = @[ genTestObject(@1, @3), genTestObject(@2, @3), ]; [self.adapter reloadObjects:newObjects]; XCTAssertEqual(sectionController1.item, newObjects[0]); XCTAssertEqual(sectionController2.item, newObjects[1]); XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[0]] != NSNotFound); XCTAssertTrue([[self.adapter.sectionMap objects] indexOfObjectIdenticalTo:newObjects[1]] != NSNotFound); } - (void)test_whenReloadingItems_withPerformUpdates_thatReloadIsApplied { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), genTestObject(@3, @3), ]]; IGTestObject *object = self.dataSource.objects[0]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; // using performBatchAnimated: to mimic re-entrant item reload [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @4; // from @1 [self.adapter reloadObjects:@[object]]; } completion:nil]; // object is moved from position 0 to 1 // it is also mutated in the previous update block AND queued for a reload self.dataSource.objects = @[ genTestObject(@3, @3), object, genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); // reloaded section XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingItems_inItemUpdateBlock_thatDoesntCrash { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestObject *object = self.dataSource.objects[1]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; sectionController.itemUpdateBlock = ^{ // We shouldn't trigger updates within -didUpdateToObject, but in case we do, // we should at least try to resolve it correctly. [self.adapter reloadObjects:@[object]]; }; // Move object from index 1 -> 2, so the -reloadObjects will need to use the previous index (1) self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@3, @3), genTestObject(@2, @2), // Create a new object to trigger -didUpdateToObject ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { // Most importantly, we don't want to crash, but lets also check the order is correct XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerMutates_whenThereIsNoWindow_thatCollectionViewCountsAreUpdated { // remove the collection view from self.window so that we use reloadData [self.collectionView removeFromSuperview]; [self setupWithObjects:@[ genTestObject(@1, @8) ]]; IGTestObject *object = self.dataSource.objects[0]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; // using performBatchAnimated: to mimic re-entrant item reload [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @6; // from @1 [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(5, 3)]]; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 6); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withoutSettingDataSource_thatCompletionBlockExecutes { UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter.collectionView = collectionView; self.dataSource.objects = @[ genTestObject(@1, @1) ]; XCTestExpectation *expectation = genExpectation; // call -performUpdatesAnimated: before we have set the data source [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { // since the data source isnt set, we complete syncronously. dispatch_async simulates setting the data source // in a different runloop from the completion block so it should be set by the time we make our subsequent // -performUpdatesAnimated: call dispatch_async(dispatch_get_main_queue(), ^{ self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @2) ]; [adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([collectionView numberOfSections], 2); [expectation fulfill]; }]; }); }]; // setting the data source immediately queries it, since the collection view is also set adapter.dataSource = self.dataSource; // simulate display reloading data on the collection view [collectionView layoutIfNeeded]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withItemsMovingInBlocks_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @0), genTestObject(@2, @7), genTestObject(@3, @8), genTestObject(@4, @8), genTestObject(@5, @8), genTestObject(@6, @5), genTestObject(@7, @8), genTestObject(@8, @8), genTestObject(@9, @8), ]]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.dataSource = self.dataSource; adapter.collectionView = collectionView; [collectionView layoutSubviews]; XCTAssertEqual([collectionView numberOfSections], 9); self.dataSource.objects = @[ genTestObject(@1, @0), genTestObject(@10, @5), genTestObject(@11, @7), genTestObject(@2, @7), genTestObject(@3, @8), genTestObject(@6, @5), // "moves" in front of 4, 5 but doesn't change index in array genTestObject(@4, @8), genTestObject(@5, @8), genTestObject(@7, @8), genTestObject(@8, @8), ]; XCTestExpectation *expectation = genExpectation; [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([collectionView numberOfSections], 10); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenItemDeleted_withDisplayDelegate_thatDelegateReceivesDeletedItem { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestObject *object = self.dataSource.objects[0]; self.dataSource.objects = @[ genTestObject(@2, @2), ]; IGTestCell *const cell = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:genIndexPath(0, 0)]; id mockDisplayHandler = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; self.adapter.delegate = mockDisplayHandler; [[mockDisplayHandler expect] listAdapter:self.adapter didEndDisplayingObject:object atIndex:0]; [[mockDisplayHandler expect] listAdapter:self.adapter didEndDisplayingObject:object cell: cell atIndexPath: [NSIndexPath indexPathForItem:0 inSection:0]]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { [mockDisplayHandler verify]; XCTAssertTrue(finished2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenItemReloaded_withDisplacingMutations_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1), genTestObject(@5, @1), ]]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @2), // reloaded genTestObject(@5, @2), // reloaded genTestObject(@4, @2), // reloaded genTestObject(@3, @1), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(finished); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenCollectionViewAppears_thatWillDisplayEventsAreSent { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; XCTAssertEqual(ic1.willDisplayCount, 1); XCTAssertEqual(ic1.didEndDisplayCount, 0); XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; XCTAssertEqual(ic2.willDisplayCount, 1); XCTAssertEqual(ic2.didEndDisplayCount, 0); XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 0); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 0); } - (void)test_whenAdapterUpdates_withItemUpdated_thatdidEndDisplayEventsAreSent { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1), // reloaded w/ 1 cell removed ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual(ic1.willDisplayCount, 1); XCTAssertEqual(ic1.didEndDisplayCount, 0); XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); XCTAssertEqual(ic2.willDisplayCount, 1); XCTAssertEqual(ic2.didEndDisplayCount, 0); XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAdapterUpdates_withItemRemoved_thatdidEndDisplayEventsAreSent { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; self.dataSource.objects = @[ genTestObject(@1, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual(ic1.willDisplayCount, 1); XCTAssertEqual(ic1.didEndDisplayCount, 0); XCTAssertEqual([ic1.willDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 0); XCTAssertEqual(ic2.willDisplayCount, 1); XCTAssertEqual(ic2.didEndDisplayCount, 1); XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic2.willDisplayCellIndexes countForObject:@1], 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAdapterUpdates_withEmptyItems_thatdidEndDisplayEventsAreSent { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; IGTestDelegateController *ic1 = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; IGTestDelegateController *ic2 = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; self.dataSource.objects = @[]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual(ic1.didEndDisplayCount, 1); XCTAssertEqual([ic1.didEndDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual(ic2.didEndDisplayCount, 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@0], 1); XCTAssertEqual([ic2.didEndDisplayCellIndexes countForObject:@1], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenBatchUpdating_withCellQuery_thatCellIsNil { __block BOOL executed = NO; __weak __typeof__(self) weakSelf = self; void (^block)(IGTestDelegateController *) = ^(IGTestDelegateController *ic) { executed = YES; XCTAssertNil([weakSelf.adapter cellForItemAtIndex:0 sectionController:ic]); }; ((IGTestDelegateDataSource *)self.dataSource).cellConfigureBlock = block; [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), ]]; // delete the last object from the original array self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@4, @1), genTestObject(@5, @1), genTestObject(@6, @1), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withWorkingRange_thatAccessingCellDoesntCrash { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), ]]; // section controller try to access a cell in -listAdapter:sectionControllerWillEnterWorkingRange: // add items beyond the 100x100 frame so they access unavailable cells self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1), genTestObject(@5, @1), genTestObject(@6, @1), genTestObject(@7, @1), genTestObject(@8, @1), genTestObject(@9, @1), genTestObject(@10, @1), genTestObject(@11, @1), ]; XCTestExpectation *expectation = genExpectation; // this will call -collectionView:performBatchUpdates:, trigger collectionView:willDisplayCell:forItemAtIndexPath:, // which kicks off the working range logic [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingItems_withDeleteAndInsertCollision_thatUpdateCanBeApplied { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @5), genTestObject(@3, @1), ]]; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; XCTestExpectation *expectation = genExpectation; [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { [batchContext deleteInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]]; [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)]]; [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; } completion:^(BOOL finished) { [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingItems_withSectionInsertedInFront_thatUpdateCanBeApplied { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @5), genTestObject(@3, @1), ]]; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; XCTestExpectation *expectation1 = genExpectation; [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; } completion:^(BOOL finished) { [expectation1 fulfill]; }]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@4, @1), // insert to shift object @2 genTestObject(@2, @5), genTestObject(@3, @1), ]; XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingItems_withSectionDeletedInFront_thatUpdateCanBeApplied { [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @5), genTestObject(@3, @1), ]]; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[1]]; XCTestExpectation *expectation1 = genExpectation; [section.collectionContext performBatchAnimated:NO updates:^(id batchContext) { [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 4)]]; } completion:^(BOOL finished) { [expectation1 fulfill]; }]; self.dataSource.objects = @[ genTestObject(@2, @5), genTestObject(@3, @1), ]; XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMovingItems_withObjectMoving_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), ]]; __block BOOL executed = NO; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; executed = YES; } completion:nil]; self.dataSource.objects = @[ genTestObject(@3, @2), genTestObject(@1, @2), genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(executed); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMovingItems_withObjectReloaded_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; __block BOOL executed = NO; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; executed = YES; } completion:nil]; self.dataSource.objects = @[ genTestObject(@1, @3), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(executed); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMovingItems_withObjectDeleted_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; __block BOOL executed = NO; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; executed = YES; } completion:nil]; self.dataSource.objects = @[]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(executed); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMovingItems_withObjectInsertedBefore_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; __block BOOL executed = NO; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; executed = YES; } completion:nil]; [self setupWithObjects:@[ genTestObject(@2, @2), genTestObject(@1, @2), ]]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(executed); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMovingItems_thatCollectionViewWorks { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; cell1.label.text = @"foo"; cell2.label.text = @"bar"; XCTestExpectation *expectation = genExpectation; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; } completion:^(BOOL finished) { IGTestCell *movedCell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *movedCell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; XCTAssertEqualObjects(movedCell1.label.text, @"bar"); XCTAssertEqualObjects(movedCell2.label.text, @"foo"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenInvalidatingSectionController_withSizeChange_thatCellsAreSameInstance_thatCellsFrameChanged { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; NSIndexPath *path1 = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *path2 = [NSIndexPath indexPathForItem:1 inSection:0]; IGTestCell *cell1 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path1]; IGTestCell *cell2 = (IGTestCell*)[self.collectionView cellForItemAtIndexPath:path2]; XCTAssertEqual(cell1.frame.size.height, 10); XCTAssertEqual(cell2.frame.size.height, 10); IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.lastObject]; section.height = 20.0; XCTestExpectation *expectation = genExpectation; [section.collectionContext invalidateLayoutForSectionController:section completion:^(BOOL finished) { XCTAssertEqual(cell1, [self.collectionView cellForItemAtIndexPath:path1]); XCTAssertEqual(cell2, [self.collectionView cellForItemAtIndexPath:path2]); XCTAssertEqual(cell1.frame.size.height, 20); XCTAssertEqual(cell2.frame.size.height, 20); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAdaptersSwapCollectionViews_thatOldAdapterDoesntUpdateOldCollectionView { IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new]; dataSource1.objects = @[genTestObject(@1, @2)]; adapter1.dataSource = dataSource1; adapter1.collectionView = self.collectionView; [self.collectionView layoutIfNeeded]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new]; dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)]; adapter2.dataSource = dataSource2; adapter2.collectionView = self.collectionView; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); dataSource1.objects = @[genTestObject(@1, @2), genTestObject(@2, @2), genTestObject(@3, @2), genTestObject(@4, @2)]; XCTestExpectation *expectation = genExpectation; [adapter1 performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAdaptersSwapCollectionViews_ { IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new]; dataSource1.objects = @[genTestObject(@1, @2)]; adapter1.dataSource = dataSource1; adapter1.collectionView = self.collectionView; [self.collectionView layoutIfNeeded]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new]; dataSource2.objects = @[genTestObject(@1, @1), genTestObject(@2, @1)]; adapter2.dataSource = dataSource2; adapter2.collectionView = self.collectionView; [self.collectionView layoutIfNeeded]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); dataSource2.objects = @[genTestObject(@1, @2), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1)]; XCTestExpectation *expectation = genExpectation; [adapter2 performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenDidUpdateAsyncReloads_withBatchUpdatesInProgress_thatReloadIsExecuted { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGTestDelegateController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; XCTestExpectation *expectation1 = genExpectation; __weak __typeof__(section) weakSection = section; section.itemUpdateBlock = ^{ // currently inside -[IGListSectionController didUpdateToObject:], change the item (note: NEVER do this) manually // so that the data powering numberOfItems changes (1 to 2). dispatch_async the update to skip outside of the // -[UICollectionView performBatchUpdates:completion:] block execution [weakSection.collectionContext performBatchAnimated:NO updates:^(id batchContext) { weakSection.item = genTestObject(@1, @2); [batchContext reloadSectionController:weakSection]; } completion:^(BOOL finished) { [expectation1 fulfill]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); }]; }; // add an object so that a batch update is triggered (diff result has changes) self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { // verify that the section still has 2 items since this completion executes AFTER the reload block above XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenInsertingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { [self setupWithObjects:@[ genTestObject(@1, @2), ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @4; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; [batchContext insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenDeletingItemsTwice_withDataUpdatedTwice_thatAllUpdatesAppliedWithoutException { [self setupWithObjects:@[ genTestObject(@1, @4), ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingSameItemTwice_thatDeletesAndInsertsAreBalanced { [self setupWithObjects:@[ genTestObject(@1, @4), ]]; IGTestObject *object = self.dataSource.objects[0]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expectation = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; [batchContext reloadInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdateQueuedDuringBatch_thatUpdateCompletesWithoutCrashing { [self setupWithObjects:@[ genTestObject(@1, @4), genTestObject(@2, @4), genTestObject(@3, @4), genTestObject(@4, @4), ]]; IGTestObject *object = self.dataSource.objects[0]; IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; XCTestExpectation *expect1 = genExpectation; XCTestExpectation *expect2 = genExpectation; [sectionController.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; self.dataSource.objects = @[ genTestObject(@2, @4), genTestObject(@4, @4), genTestObject(@1, @3), ]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); [expect1 fulfill]; }]; } completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:3], 4); [expect2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenMassiveUpdate_thatUpdateApplied { // init empty [self setupWithObjects:@[]]; NSMutableArray *objects = [NSMutableArray new]; for (NSInteger i = 0; i < 3000; i++) { [objects addObject:genTestObject(@(i + 1), @4)]; } self.dataSource.objects = objects; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3000); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesAnimated_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, YES); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); XCTAssertEqual(listener2.hits, 1); XCTAssertEqual(listener2.animated, YES); XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_withPerformUpdatesNotAnimated_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, NO); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); XCTAssertEqual(listener2.hits, 1); XCTAssertEqual(listener2.animated, NO); XCTAssertEqual(listener2.type, IGListAdapterUpdateTypePerformUpdates); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_withReloadData_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, NO); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeReloadData); XCTAssertEqual(listener2.hits, 1); XCTAssertEqual(listener2.animated, NO); XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeReloadData); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesAnimated_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTestExpectation *expectation = genExpectation; [section.collectionContext performBatchAnimated:YES updates:^(id _Nonnull batchContext) { [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, YES); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); XCTAssertEqual(listener2.hits, 1); XCTAssertEqual(listener2.animated, YES); XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_withItemUpdatesNotAnimated_thatEventsReceived { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; IGListSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTestExpectation *expectation = genExpectation; [section.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, NO); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypeItemUpdates); XCTAssertEqual(listener2.hits, 1); XCTAssertEqual(listener2.animated, NO); XCTAssertEqual(listener2.type, IGListAdapterUpdateTypeItemUpdates); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingMultipleUpdateListeners_thenRemovingListener_thatRemainingReceives { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener1 = [IGListAdapterUpdateTester new];; IGListAdapterUpdateTester *listener2 = [IGListAdapterUpdateTester new];; [self.adapter addUpdateListener:listener1]; [self.adapter addUpdateListener:listener2]; [self.adapter removeUpdateListener:listener2]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual(listener1.hits, 1); XCTAssertEqual(listener1.animated, YES); XCTAssertEqual(listener1.type, IGListAdapterUpdateTypePerformUpdates); XCTAssertEqual(listener2.hits, 0); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenAddingUpdateListener_thenListenerReferenceHitsZero_thatListenerReleased { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; IGListAdapterUpdateTester *listener = [IGListAdapterUpdateTester new]; __weak id weakListener = listener; [self.adapter addUpdateListener:listener]; listener = nil; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertNil(weakListener); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenModifyingInitialAndFinalAttribute_thatLayoutIsCorrect { // set up the custom layout IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; self.collectionView.collectionViewLayout = layout; IGTestObject *object = genTestObject(@1, @2); [self setupWithObjects:@ [object]]; // set up the section controller IGTestDelegateController *sectionController = [self.adapter sectionControllerForObject:object]; sectionController.transitionDelegate = sectionController; CGPoint offset = CGPointMake(10, 10); NSIndexPath *indexPath = genIndexPath(0, 0); UICollectionViewLayoutAttributes *attribute = [layout layoutAttributesForItemAtIndexPath:indexPath]; // set up the custom initial attribute transformation sectionController.initialAttributesOffset = offset; UICollectionViewLayoutAttributes *initialAttribute = [layout initialLayoutAttributesForAppearingItemAtIndexPath:indexPath]; // set up the custom final attribute transformation sectionController.finalAttributesOffset = offset; UICollectionViewLayoutAttributes *finalAttribute = [layout finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath]; IGAssertEqualPoint(initialAttribute.center, attribute.center.x + offset.x, attribute.center.y + offset.y); IGAssertEqualPoint(finalAttribute.center, attribute.center.x + offset.x ,attribute.center.y + offset.y); } - (void)test_whenModifyingInitialAndFinalAttribute_withoutTransitionDelegate_thatLayoutIsCorrect { // set up the custom layout IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; self.collectionView.collectionViewLayout = layout; IGTestObject *object = genTestObject(@1, @2); [self setupWithObjects:@ [object]]; // When no transition delegate is set, the initial and final layout methods no-op, so these values should all match NSIndexPath *indexPath = genIndexPath(0, 0); UICollectionViewLayoutAttributes *attribute = [layout layoutAttributesForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *initialAttribute = [layout initialLayoutAttributesForAppearingItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *finalAttribute = [layout finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath]; IGAssertEqualPoint(attribute.center, initialAttribute.center.x, initialAttribute.center.y); IGAssertEqualPoint(attribute.center, finalAttribute.center.x, finalAttribute.center.y); } - (void)test_whenSwappingCollectionViewsAfterUpdate_thatUpdatePerformedOnTheCorrectCollectionView { // BEGIN: setup of FIRST adapter+dataSource+collectionView IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; UICollectionView *collectionView1 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView1]; adapter1.collectionView = collectionView1; IGTestDelegateDataSource *dataSource1 = [IGTestDelegateDataSource new]; dataSource1.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; adapter1.dataSource = dataSource1; // END: setup of FIRST adapter+dataSource+collectionView // BEGIN: setup of SECOND adapter+dataSource+collectionView IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView2]; adapter2.collectionView = collectionView2; IGTestDelegateDataSource *dataSource2 = [IGTestDelegateDataSource new]; dataSource2.objects = @[ genTestObject(@3, @1) ]; adapter2.dataSource = dataSource2; // END: setup of SECOND adapter+dataSource+collectionView // delete the last-most section from the FIRST dataSource dataSource1.objects = @[ genTestObject(@1, @1) ]; XCTestExpectation *expectation = genExpectation; [adapter1 performUpdatesAnimated:YES completion:^(BOOL finished) { [expectation fulfill]; }]; // simulate a collectionView swap (e.g. cell reuse) immediately after an async update is queued adapter1.collectionView = collectionView2; adapter2.collectionView = collectionView1; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenCollectionViewBecomesNilDuringPerformUpdates_thatStateCleanedCorrectly { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; // perform update on listAdapter XCTestExpectation *expectation1 = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { [expectation1 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; // update the underlying contents before performing another update self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; // perform update, but set the listAdapter's collectionView to nil during the update XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { [expectation2 fulfill]; }]; self.adapter.collectionView = nil; [self waitForExpectationsWithTimeout:30 handler:nil]; // add a new collectionView to the listAdapter UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView2]; self.adapter.collectionView = collectionView2; // update the underlying contents before performing update self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1) ]; // perform update on listAdapter (now with a non-nil collectionView) XCTestExpectation *expectation3 = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { [expectation3 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenCollectionViewBecomesNilDuringReloadData_thatStateCleanedCorrectly { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; // reload data on listAdapter XCTestExpectation *expectation1 = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { [expectation1 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; // update the underlying contents before reloading again self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1) ]; // reload data, but set the listAdapter's collectionView to nil during the update XCTestExpectation *expectation2 = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { [expectation2 fulfill]; }]; self.adapter.collectionView = nil; [self waitForExpectationsWithTimeout:30 handler:nil]; // add a new collectionView to the listAdapter UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView2]; self.adapter.collectionView = collectionView2; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1) ]; // reload data on listAdapter (now with a non-nil collectionView) XCTestExpectation *expectation3 = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { [expectation3 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdating_withMissingSectionController_thatDoesNotCrash { [self setupWithObjects:@[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]]; // Adding an object that won't have a corresponding section-controller self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar"), kIGTestDelegateDataSourceSkipObject ]; // Perform updates on the adapter XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // Checked that the update worked XCTAssertTrue(finished); // Check that we skipped the object with a missing section-controller XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Dealloc checks - (void)test_whenReleasingObjects_thatAssertDoesntFire { [self setupWithObjects:@[ genTestObject(@1, @1) ]]; // if the adapter keeps a strong ref to self and uses an async method, this will hit asserts that a list item // controller is nil. the adapter should be released and the completion block never called. @autoreleasepool { IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:2]; adapter.collectionView = self.collectionView; adapter.dataSource = self.dataSource; [adapter performUpdatesAnimated:NO completion:^(BOOL finished) { XCTFail(@"Should not reach completion block for adapter"); }]; } self.collectionView = nil; self.dataSource = nil; // queued after perform updates XCTestExpectation *expectation = genExpectation; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [expectation fulfill]; }); [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes { IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; dataSource.objects = @[genTestObject(@1, @1)]; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = dataSource; [self.collectionView layoutIfNeeded]; dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; dataSource = nil; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenQueuingUpdate_withSectionControllerBatchUpdate_thatSectionControllerNotRetained { __weak id weakSectionController = nil; __weak id weakAdapter = nil; __weak id weakCollectionView = nil; @autoreleasepool { IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new]; IGTestObject *object = genTestObject(@1, @2); dataSource.objects = @[object]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; [collectionView layoutIfNeeded]; XCTAssertEqual([collectionView numberOfSections], 1); XCTAssertEqual([collectionView numberOfItemsInSection:0], 2); IGListSectionController *section = [adapter sectionControllerForObject:object]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { object.value = @3; [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished) {}]; dataSource.objects = @[object, genTestObject(@2, @2)]; [adapter performUpdatesAnimated:YES completion:^(BOOL finished) {}]; weakAdapter = adapter; weakCollectionView = collectionView; weakSectionController = section; XCTAssertNotNil(weakAdapter); XCTAssertNotNil(weakCollectionView); XCTAssertNotNil(weakSectionController); } XCTAssertNil(weakAdapter); XCTAssertNil(weakCollectionView); XCTAssertNil(weakSectionController); } - (void)test_whenInvalidatingInsideBatchUpdate_withSystemReleased_thatSystemNil_andCollectionViewDoesntCrashOnDealloc { __weak id weakAdapter = nil; __block BOOL executedItemUpdate = NO; XCTestExpectation *expectation = genExpectation; @autoreleasepool { self.dataSource.objects = @[ genTestObject(@1, @"Bar"), genTestObject(@0, @"Foo") ]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.dataSource = self.dataSource; adapter.collectionView = collectionView; [collectionView layoutIfNeeded]; IGTestDelegateController *section = [adapter sectionControllerForObject:self.dataSource.objects.firstObject]; __weak typeof(section) weakSection = section; section.itemUpdateBlock = ^{ executedItemUpdate = YES; [weakSection.collectionContext invalidateLayoutForSectionController:weakSection completion:nil]; }; self.dataSource.objects = @[ genTestObject(@1, @"Bar"), genTestObject(@0, @"Foo") ]; [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertNotNil(collectionView); XCTAssertNotNil(adapter); [collectionView removeFromSuperview]; [expectation fulfill]; }]; weakAdapter = adapter; XCTAssertNotNil(weakAdapter); } [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { XCTAssertTrue(executedItemUpdate); XCTAssertNil(weakAdapter); }]; } - (void)test_whenInvalidatingInsideBatchUpdate_andRemoveThatSectionController_thatCollectionViewDoesntCrash { IGTestObject *foo = genTestObject(@1, @"Foo"); IGTestObject *bar = genTestObject(@0, @"Bar"); self.dataSource.objects = @[foo, bar]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:[UICollectionViewFlowLayout new]]; [self.window addSubview:collectionView]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.dataSource = self.dataSource; adapter.collectionView = collectionView; [collectionView layoutIfNeeded]; IGTestDelegateController *sectionToRemove = [adapter sectionControllerForObject:bar]; self.dataSource.objects = @[foo]; XCTestExpectation *expectation = genExpectation; [adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertTrue(finished); [expectation fulfill]; }]; XCTestExpectation *expectation2 = genExpectation; [sectionToRemove.collectionContext invalidateLayoutForSectionController:sectionToRemove completion:^(BOOL finished) { // That section-controller is about to be removed, so this should not finish. XCTAssertFalse(finished); [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingBatchSectionUpdate_thatTransactionObjectsGetsDeallocated { __weak IGListUpdateTransactionBuilder *transactionBuilder = nil; __block __weak IGListUpdateTransactionBuilder *lastTransactionBuilder = nil; __block __weak id transaction = nil; IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; @autoreleasepool { [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // Grab the current builder transactionBuilder = [updater transactionBuilder]; [self.adapter performBatchAnimated:NO updates:^(id _Nonnull batchContext) { // Take advantage of `performBatchAnimated` to grab the transaction, but we don't perform any changes. lastTransactionBuilder = [updater lastTransactionBuilder]; XCTAssertNotNil(lastTransactionBuilder); transaction = [updater transaction]; XCTAssertNotNil(transaction); } completion:nil]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertNil(transactionBuilder); XCTAssertNil(lastTransactionBuilder); XCTAssertNil(transaction); [expectation fulfill]; }); }]; // Force the update to happen right away [updater update]; } [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Changing the collectionView/dataSource - (void)test_whenChangingDataSourceWithADifferentCount_thenPerformBatchUpdate_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section self.dataSource = [IGTestDelegateDataSource new]; self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; self.adapter.dataSource = self.dataSource; // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: Invalidated count // Schedule update XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: 2 sections [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenChangingCollectionView_thenScheduleSectionUpdate_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section // Force dataSource <> adapater sync by changing the collection view self.layout = [UICollectionViewFlowLayout new]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:self.layout]; self.adapter.collectionView = self.collectionView; // STATE // DataSource: 1 sections // Adapter: 1 sections // CollectionView: Invalidated count XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual(self.adapter.objects.count, 1); // STATE // DataSource: 1 sections // Adapter: 1 sections // CollectionView: 1 sections } - (void)test_settingCollectionViewAndDataSource_thatDontCreateCellsUntilLayout { self.dataSource.objects = @[ genTestObject(@0, @"Foo") ]; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self.dataSource; // Make sure we didn't create the cells just yet, since we might want to scroll way without animating. XCTAssertNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); [self.collectionView layoutIfNeeded]; XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } #pragma mark - Changing the collectionView/dataSource with pending SECTION updates - (void)test_whenSchedulingSectionUpdate_thenChangeCollectionView_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // STATE // DataSource: 2 sections // Adapter: 1 section // CollectionView: 1 section // Schedule update XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: Invalidated count // Force collectionView <> adapter sync XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); XCTAssertTrue(finished); // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: 2 sections [expectation fulfill]; }]; // Force dataSource <> adapater sync by changing the collection view self.layout = [UICollectionViewFlowLayout new]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:self.layout]; self.adapter.collectionView = self.collectionView; // Although all the syncs should have been checked by now, lets still make // sure the counts are right. XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSchedulingSectionUpdate_thenChangeTheDataSource_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // STATE // DataSource: 2 section // Adapter: 1 section // CollectionView: 1 section // Schedule update XCTestExpectation *expectation2 = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // STATE // DataSource: 3 sections // Adapter: 3 sections // CollectionView: Invalidated count XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(self.adapter.objects.count, 3); // STATE // DataSource: 3 sections // Adapter: 3 sections // CollectionView: 3 sections [expectation2 fulfill]; }]; // Force dataSource <> adapater sync by changing the dataSource self.dataSource = [IGTestDelegateDataSource new]; self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar"), genTestObject(@2, @"Baz") ]; self.adapter.dataSource = self.dataSource; // Although all the syncs should have been checked by now, lets still make // sure the counts are right. XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(self.adapter.objects.count, 3); [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Changing the collectionView/dataSource with pending ITEM updates - (void)test_whenSchedulingItemUpdate_thenChangeCollectionView_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @1) ]]; // STATE // Section Controller: 1 cell // CollectionView: 1 cell IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; XCTAssertNotNil(contoller); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTestExpectation *expectation1 = genExpectation; [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { // Just change the item count for section 0 contoller.item = genTestObject(@0, @2); [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished) { XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); [expectation1 fulfill]; }]; // Force dataSource <> adapater sync by changing the collection view self.layout = [UICollectionViewFlowLayout new]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:self.layout]; self.adapter.collectionView = self.collectionView; // STATE // Section Controller: 2 cells // CollectionView: Invalidated count XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); // STATE // Section Controller: 2 cells // CollectionView: 2 cells [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSchedulingItemUpdate_thenChangeDataSource_thatLastestDataIsApplied { [self setupWithObjects:@[ genTestObject(@0, @1) ]]; // STATE // Section Controller: 1 cell // CollectionView: 1 cell IGTestDelegateController *contoller = (IGTestDelegateController *)[self.adapter sectionControllerForSection:0]; XCTAssertNotNil(contoller); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTestExpectation *expectation1 = genExpectation; [contoller.collectionContext performBatchAnimated:NO updates:^(id _Nonnull batchContext) { // Just change the item count for section 0 contoller.item = genTestObject(@0, @2); [batchContext insertInSectionController:contoller atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:^(BOOL finished) { XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); [expectation1 fulfill]; }]; // Force dataSource <> adapater sync by changing the dataSource. // Note that we keep the old object here, but that should not matter since // it didn't change, it won't call -didUpdateToObject on that section-controller. IGTestDelegateDataSource *oldDataSource = self.dataSource; self.dataSource = [IGTestDelegateDataSource new]; self.dataSource.objects = oldDataSource.objects; self.adapter.dataSource = self.dataSource; // STATE // Section Controller: 2 cells // CollectionView: Invalidated count XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); // STATE // Section Controller: 2 cells // CollectionView: 2 cells [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Changing the collectionView/dataSource in middle of diffing - (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeCollectionView_thatLastestDataIsApplied { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // STATE // DataSource: 2 sections // Adapter: 1 section // CollectionView: 1 section // Schedule update XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: Invalidated count XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); // STATE // DataSource: 2 sections // Adapter: 2 sections // CollectionView: 2 sections [expectation fulfill]; }]; // Force the update to happen right way, so that the diffing starts [updater update]; // Force dataSource <> adapater sync by changing the collection view self.layout = [UICollectionViewFlowLayout new]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:self.layout]; self.adapter.collectionView = self.collectionView; // Although all the syncs should have been checked by now, lets still make // sure the counts are right. XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSchedulingSectionUpdate_thenBeginDiffing_thenChangeTheDataSource_thatLastestDataIsApplied { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; [self setupWithObjects:@[ genTestObject(@0, @"Foo") ]]; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar") ]; // STATE // DataSource: 2 sections // Adapter: 1 section // CollectionView: 1 section // Schedule update XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // STATE // DataSource: 3 sections // Adapter: 3 sections // CollectionView: Invalidated count XCTAssertTrue(finished); XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(self.adapter.objects.count, 3); // STATE // DataSource: 3 sections // Adapter: 3 sections // CollectionView: 3 sections [expectation fulfill]; }]; // Force the update to happen right way, so that the diffing starts [updater update]; // Force dataSource <> adapater sync by changing the dataSource self.dataSource = [IGTestDelegateDataSource new]; self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar"), genTestObject(@2, @"Baz") ]; self.adapter.dataSource = self.dataSource; // Although all the syncs should have been checked by now, lets still make // sure the counts are right. XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(self.adapter.objects.count, 3); [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Sync the collectionView before setting a adapter.dataSource - (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thatLastestDataIsApplied { self.adapter.collectionView = self.collectionView; // Force the adapter <> collectionView to sync XCTAssertEqual([self.collectionView numberOfSections], 0); XCTAssertEqual([self.adapter objects].count, 0); // STATE // DataSource: Nil // Adapter: 0 sections // CollectionView: 0 sections // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and // invalidate the collectionView's internal section/item counts. self.dataSource.objects = @[genTestObject(@1, @"Foo")]; self.adapter.dataSource = self.dataSource; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.adapter objects].count, 1); // Test that collectionView syncs with the adapter [self.collectionView layoutIfNeeded]; XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: 1 section } - (void)test_whenCollectionViewSyncsBeforeTheAdapterDataSourceIsSet_thenSchedulingSectionUpdate_thatLastestDataIsApplied { self.adapter.collectionView = self.collectionView; // Force the adapter <> collectionView to sync XCTAssertEqual([self.collectionView numberOfSections], 0); XCTAssertEqual([self.adapter objects].count, 0); // STATE // DataSource: Nil // Adapter: 0 sections // CollectionView: 0 sections // Changing the `adapter.dataSource` will sync the adapter <> dataSource, and // invalidate the collectionView's internal section/item counts. self.dataSource.objects = @[genTestObject(@0, @"Foo")]; self.adapter.dataSource = self.dataSource; // STATE // DataSource: 1 section // Adapter: 1 section // CollectionView: Invalidated counts (UICollectionView will ask for counts on next layout) XCTAssertEqual([self.adapter objects].count, 1); // Adding an object self.dataSource.objects = @[ genTestObject(@0, @"Foo"), genTestObject(@1, @"Bar"), ]; // STATE // DataSource: 2 sections // Adapter: 1 section // CollectionView: Invalidated counts (Still) // Test that a batchUpdate from 1 -> 2 objects works, even though // the collectionView has not synced yet. XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { // Checked that the update worked XCTAssertTrue(finished); // Check that the we have the correct counts XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(self.adapter.objects.count, 2); [expectation fulfill]; // STATE // DataSource: 2 sections // Adapter: 2 section // CollectionView: 2 sections }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSectionControllerNotSubclassed_thatDoesNotCrash { // We need a custom layout that creates attributes for all cells, even the ones with size // zero, so that the UICollectionView requests all cells. Using `UICollectionViewDelegateFlowLayout` // doesn't crash, because it doesn't seem to return attributes where the size is zero. self.collectionView.collectionViewLayout = [IGListTestCollectionViewLayout new]; XCTExpectFailureWithOptions(@"When IGListSectionController isn't subclassed, expect an assertion failure, but avoid a crash.", [XCTExpectedFailureOptions nonStrictOptions]); [self setupWithObjects:@[kIGTestDelegateDataSourceNoSectionControllerSubclass]]; XCTestExpectation *expectation = genExpectation; [self.adapter reloadDataWithCompletion:^(BOOL finished) { [self.collectionView layoutIfNeeded]; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveDiffing_thatCollectionViewCountsUpdate { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; updater.adaptiveDiffingExperimentConfig = (IGListAdaptiveDiffingExperimentConfig) { .enabled = YES, }; [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), genTestObject(@3, @3), ]]; self.dataSource.objects = @[ genTestObject(@2, @2), genTestObject(@1, @1), // moved from index 0 to 1 genTestObject(@3, @3), genTestObject(@4, @4), // new ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:3], 4); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveCoalescing_thatCollectionViewCountsUpdate { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.adaptiveCoalescingExperimentConfig = (IGListAdaptiveCoalescingExperimentConfig) { .enabled = YES, }; [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), genTestObject(@3, @3), ]]; self.dataSource.objects = @[ genTestObject(@2, @2), genTestObject(@1, @1), // moved from index 0 to 1 genTestObject(@3, @3), genTestObject(@4, @4), // new ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 4); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:3], 4); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveDiffingHigherQOS_thatCollectionViewUpdates { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; updater.adaptiveDiffingExperimentConfig = (IGListAdaptiveDiffingExperimentConfig) { .enabled = YES, .higherQOSEnabled = YES, .maxItemCountToRunOnMain = 0, .lowerPriorityWhenViewNotVisible = NO }; [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; self.dataSource.objects = @[ genTestObject(@2, @2), genTestObject(@3, @3), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveDiffingLowerPriorityWhenNotVisible_thatCollectionViewUpdates { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; updater.adaptiveDiffingExperimentConfig = (IGListAdaptiveDiffingExperimentConfig) { .enabled = YES, .higherQOSEnabled = NO, .maxItemCountToRunOnMain = 0, .lowerPriorityWhenViewNotVisible = YES }; // Remove from window to make it "not visible" [self.collectionView removeFromSuperview]; [self setupWithObjects:@[ genTestObject(@1, @1), genTestObject(@2, @2), ]]; self.dataSource.objects = @[ genTestObject(@2, @2), genTestObject(@3, @3), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveDiffingSmallItemCount_thatDiffRunsOnMain { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = YES; updater.adaptiveDiffingExperimentConfig = (IGListAdaptiveDiffingExperimentConfig) { .enabled = YES, .higherQOSEnabled = NO, .maxItemCountToRunOnMain = 100, // Item count is under this threshold .lowerPriorityWhenViewNotVisible = NO }; [self setupWithObjects:@[ genTestObject(@1, @1), ]]; self.dataSource.objects = @[ genTestObject(@1, @1), genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdates_withAdaptiveDiffingBackgroundDisabled_thatDiffRunsOnMain { IGListAdapterUpdater *updater = (IGListAdapterUpdater *)self.updater; updater.allowsBackgroundDiffing = NO; updater.adaptiveDiffingExperimentConfig = (IGListAdaptiveDiffingExperimentConfig) { .enabled = YES, .higherQOSEnabled = YES, .maxItemCountToRunOnMain = 0, .lowerPriorityWhenViewNotVisible = YES }; [self setupWithObjects:@[ genTestObject(@1, @1), ]]; self.dataSource.objects = @[ genTestObject(@2, @2), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } @end ================================================ FILE: Tests/IGListAdapterProxyTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterProxy.h" @interface IGListAdapterProxyTests : XCTestCase @end @implementation IGListAdapterProxyTests - (void)test_whenSendingInterceptedMethod_thatAdapterReceivesMethod { id mockAdapter = [OCMockObject mockForClass:[IGListAdapter class]]; id mockCollectionViewDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; IGListAdapterProxy *proxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:mockCollectionViewDelegate scrollViewTarget:nil interceptor:mockAdapter]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; NSIndexPath *indexPath = [NSIndexPath new]; // method is intercepted and sent to the adapter instead [[mockAdapter expect] collectionView:collectionView didSelectItemAtIndexPath:indexPath]; [[mockCollectionViewDelegate reject] collectionView:collectionView didSelectItemAtIndexPath:indexPath]; [(id)proxy collectionView:collectionView didSelectItemAtIndexPath:indexPath]; [mockCollectionViewDelegate verify]; [mockAdapter verify]; } - (void)test_whenSendingCollectionViewDelegateMethod_thatCollectionViewDelegateReceivesMethod { id mockAdapter = [OCMockObject mockForClass:[IGListAdapter class]]; id mockCollectionViewDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; IGListAdapterProxy *proxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:mockCollectionViewDelegate scrollViewTarget:nil interceptor:mockAdapter]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; NSIndexPath *indexPath = [NSIndexPath new]; // method is not intercepted and should be sent to the delegate [[mockAdapter reject] collectionView:collectionView shouldShowMenuForItemAtIndexPath:indexPath]; [[mockCollectionViewDelegate expect] collectionView:collectionView shouldShowMenuForItemAtIndexPath:indexPath]; [(id)proxy collectionView:collectionView shouldShowMenuForItemAtIndexPath:indexPath]; [mockCollectionViewDelegate verify]; [mockAdapter verify]; } - (void)test_whenSendingScrollViewDelegateMethod_whenNoCollectionViewDelegate_thatScrollViewDelegateReceivesMethod { id mockAdapter = [OCMockObject mockForClass:[IGListAdapter class]]; id mockCollectionViewDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; id mockScrollViewDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; IGListAdapterProxy *proxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:mockCollectionViewDelegate scrollViewTarget:mockScrollViewDelegate interceptor:mockAdapter]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; // method is not intercepted and should be sent to the appropriate delegate [[mockAdapter reject] scrollViewDidZoom:collectionView]; [[mockCollectionViewDelegate reject] scrollViewDidZoom:collectionView]; [[mockScrollViewDelegate expect] scrollViewDidZoom:collectionView]; [(id)proxy scrollViewDidZoom:collectionView]; [mockCollectionViewDelegate verify]; [mockScrollViewDelegate verify]; [mockAdapter verify]; } - (void)test_whenSendingUnimplementedSelector_thatNothingBreaks { id mockAdapter = [OCMockObject mockForClass:[IGListAdapter class]]; IGListAdapterProxy *proxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:nil scrollViewTarget:nil interceptor:mockAdapter]; // this will try to forward a method to nil since there are no targets set // verify that this fails silently UIScrollView *scrollView = [UIScrollView new]; [(id)proxy scrollViewDidZoom:scrollView]; } @end ================================================ FILE: Tests/IGListAdapterStoryboardTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListAdapterInternal.h" #import "IGListTestAdapterStoryboardDataSource.h" #import "IGTestStoryboardSupplementarySource.h" #import "IGTestStoryboardViewController.h" #if !TARGET_OS_TV static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {100.0, 100.0}}; @interface IGListAdapterStoryboardTests : XCTestCase @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) IGListTestAdapterStoryboardDataSource *dataSource; @property (nonatomic, strong) IGListAdapterUpdater *updater; @property (nonatomic, strong) IGTestStoryboardViewController *viewController; @end @implementation IGListAdapterStoryboardTests - (void)setUp { [super setUp]; self.window = [[UIWindow alloc] initWithFrame:kStackTestFrame]; UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"IGTestStoryboard" bundle:[NSBundle bundleForClass:self.class]]; self.viewController = [storyboard instantiateViewControllerWithIdentifier:@"testVC"]; [self.window addSubview:self.viewController.view]; [self.viewController performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES]; self.collectionView = self.viewController.collectionView; self.dataSource = [[IGListTestAdapterStoryboardDataSource alloc] init]; self.updater = [[IGListAdapterUpdater alloc] init]; self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:self.viewController]; } - (void)tearDown { [super tearDown]; self.adapter = nil; self.collectionView = nil; self.dataSource = nil; } - (void)setupWithObjects:(NSArray *)objects { self.dataSource.objects = objects; self.adapter.collectionView = self.viewController.collectionView; self.adapter.dataSource = self.dataSource; [self.adapter reloadDataWithCompletion:nil]; IGTestStoryboardSupplementarySource *supplementarySource = [IGTestStoryboardSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:objects.firstObject]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; [self.collectionView layoutIfNeeded]; } - (void)test_whenSupplementarySourceSupportsHeader { [self setupWithObjects:@[genTestObject(@1, @"Foo")]]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } @end #endif ================================================ FILE: Tests/IGListAdapterTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import #import "IGListAdapterInternal.h" #import "IGListTestAdapterDataSource.h" #import "IGListTestAdapterHorizontalDataSource.h" #import "IGListTestAdapterReorderingDataSource.h" #import "IGListTestCase.h" #import "IGListTestOffsettingLayout.h" #import "IGListTestSection.h" #import "IGTestNibSupplementaryView.h" #import "IGTestReorderableSection.h" #import "IGTestSupplementarySource.h" #import "UICollectionViewLayout+InteractiveReordering.h" @interface IGListAdapterTests : IGListTestCase @end @implementation IGListAdapterTests - (void)setUp { self.dataSource = [IGListTestAdapterDataSource new]; self.updater = [IGListReloadDataUpdater new]; [super setUp]; // test case doesn't use -setupWithObjects for more control over update events self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self.dataSource; if (@available(iOS 11.0, tvOS 11.0, *)) { self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } } - (void)test_whenAdapterNotUpdated_withDataSourceUpdated_thatAdapterHasNoSectionControllers { self.dataSource.objects = @[@0, @1, @2]; XCTAssertNil([self.adapter sectionControllerForObject:@0]); XCTAssertNil([self.adapter sectionControllerForObject:@1]); XCTAssertNil([self.adapter sectionControllerForObject:@2]); } - (void)test_whenAdapterUpdated_thatAdapterHasSectionControllers { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTAssertNotNil([self.adapter sectionControllerForObject:@0]); XCTAssertNotNil([self.adapter sectionControllerForObject:@1]); XCTAssertNotNil([self.adapter sectionControllerForObject:@2]); } - (void)test_whenAdapterReloaded_thatAdapterHasSectionControllers { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertNotNil([self.adapter sectionControllerForObject:@0]); XCTAssertNotNil([self.adapter sectionControllerForObject:@1]); XCTAssertNotNil([self.adapter sectionControllerForObject:@2]); } - (void)test_whenAdapterUpdated_thatSectionControllerHasSection { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * list = [self.adapter sectionControllerForObject:@1]; XCTAssertEqual([self.adapter sectionForSectionController:list], 1); } - (void)test_whenAdapterUpdated_withUnknownItem_thatSectionControllerHasNoSection { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * randomList = [[IGListTestSection alloc] init]; XCTAssertEqual([self.adapter sectionForSectionController:randomList], NSNotFound); } - (void)test_whenQueryingAdapter_withUnknownItem_thatSectionControllerIsNil { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; XCTAssertNil([self.adapter sectionControllerForObject:@3]); } - (void)test_whenAdapterUpdated_thatSectionControllerHasCorrectObject { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * list = [self.adapter sectionControllerForObject:@1]; XCTAssertEqual([self.adapter objectForSectionController:list], @1); } - (void)test_whenQueryingAdapter_withUnknownItem_thatObjectForSectionControllerIsNil { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * randomList = [[IGListTestSection alloc] init]; XCTAssertNil([self.adapter objectForSectionController:randomList]); } - (void)test_whenQueryingIndexPaths_withSectionController_thatPathsAreEqual { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * second = [self.adapter sectionControllerForObject:@1]; NSArray *paths0 = [self.adapter indexPathsFromSectionController:second indexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 4)] usePreviousIfInUpdateBlock:NO]; NSArray *expected = @[ [NSIndexPath indexPathForItem:2 inSection:1], [NSIndexPath indexPathForItem:3 inSection:1], [NSIndexPath indexPathForItem:4 inSection:1], [NSIndexPath indexPathForItem:5 inSection:1], ]; XCTAssertEqualObjects(paths0, expected); } - (void)test_whenQueryingIndexPaths_insideBatchUpdateBlock_thatPathsAreEqual { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController * second = [self.adapter sectionControllerForObject:@1]; __block BOOL executed = NO; [self.adapter performBatchAnimated:YES updates:^(id batchContext) { NSArray *paths = [self.adapter indexPathsFromSectionController:second indexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(2, 2)] usePreviousIfInUpdateBlock:YES]; NSArray *expected = @[ [NSIndexPath indexPathForItem:2 inSection:1], [NSIndexPath indexPathForItem:3 inSection:1], ]; XCTAssertEqualObjects(paths, expected); executed = YES; } completion:nil]; XCTAssertTrue(executed); } - (void)test_whenReloadingEmptyIndexSet_whenInvalidatingLayout_insideBatchUpdateBlock_thatOperationExitsGracefully { self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:YES completion:nil]; IGListSectionController *second = [self.adapter sectionControllerForObject:@1]; __block BOOL executed = NO; [self.adapter performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadInSectionController:second atIndexes:[NSIndexSet indexSet]]; [batchContext invalidateLayoutInSectionController:second atIndexes:[NSIndexSet indexSet]]; executed = YES; } completion:nil]; XCTAssertTrue(executed); } - (void)test_whenQueryingReusableIdentifier_thatIdentifierEqualsClassName { NSString *identifier = IGListReusableViewIdentifier(UICollectionViewCell.class, nil, nil); XCTAssertEqualObjects(identifier, @"UICollectionViewCell"); } - (void)test_whenQueryingReusableIdentifierWithGivenIdentifier_thatIdentifierEqualsGivenIdentifierAndClassName { NSString *identifier = IGListReusableViewIdentifier(UICollectionViewCell.class, nil, @"MyCoolID"); XCTAssertEqualObjects(identifier, @"MyCoolIDUICollectionViewCell"); } - (void)test_whenQueryingReusableIdentifier_thatIdentifierEqualsClassNameAndSupplimentaryKind { NSString *identifier = IGListReusableViewIdentifier(UICollectionViewCell.class, UICollectionElementKindSectionFooter, nil); XCTAssertEqualObjects(identifier, @"UICollectionElementKindSectionFooterUICollectionViewCell"); } - (void)test_whenDataSourceChanges_thatBackgroundViewVisibilityChanges { self.dataSource.objects = @[@1]; UIView *background = [[UIView alloc] init]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background; __block BOOL executed = NO; [self.adapter reloadDataWithCompletion:^(BOOL finished) { UIView *backgroundViewAfterReload = self.adapter.collectionView.backgroundView; XCTAssertTrue(!backgroundViewAfterReload || backgroundViewAfterReload.hidden, @"Background view should be hidden"); self.dataSource.objects = @[]; [self.adapter reloadDataWithCompletion:^(BOOL finished2) { XCTAssertFalse(self.adapter.collectionView.backgroundView.hidden, @"Background view should be visible"); XCTAssertEqualObjects(background, self.adapter.collectionView.backgroundView, @"Background view not correctly assigned"); executed = YES; }]; }]; XCTAssertTrue(executed); } - (void)test_whenReloadingData_thatNewSectionControllersAreCreated { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *oldSectionController = [self.adapter sectionControllerForObject:@1]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *newSectionController = [self.adapter sectionControllerForObject:@1]; XCTAssertNotEqual(oldSectionController, newSectionController); } - (void)test_whenSettingCollectionView_thenSettingDataSource_thatViewControllerIsSet { self.dataSource.objects = @[@0, @1, @2]; UIViewController *controller = [UIViewController new]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:controller]; adapter.collectionView = self.collectionView; adapter.dataSource = self.dataSource; IGListSectionController *sectionController = [adapter sectionControllerForObject:@1]; XCTAssertEqual(controller, sectionController.viewController); } - (void)test_whenSettingCollectionView_thenSettingDataSource_thatCellExists { self.dataSource.objects = @[@1]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil]; adapter.collectionView = self.collectionView; adapter.dataSource = self.dataSource; [self.collectionView layoutIfNeeded]; XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } - (void)test_whenSettingDataSource_thenSettingCollectionView_thatCellExists { self.dataSource.objects = @[@1]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil]; adapter.dataSource = self.dataSource; adapter.collectionView = self.collectionView; [self.collectionView layoutIfNeeded]; XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } - (void)test_whenChangingCollectionViews_thatCellsExist { self.dataSource.objects = @[@1]; IGListAdapterUpdater *updater = [[IGListAdapterUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.dataSource = self.dataSource; adapter.collectionView = self.collectionView; [self.collectionView layoutIfNeeded]; XCTAssertNotNil([self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); UICollectionView *otherCollectionView = [[UICollectionView alloc] initWithFrame:self.collectionView.frame collectionViewLayout:self.collectionView.collectionViewLayout]; adapter.collectionView = otherCollectionView; [otherCollectionView layoutIfNeeded]; XCTAssertNotNil([otherCollectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); } - (void)test_whenChangingCollectionViewsToACollectionViewInUseByAnotherAdapter_thatCollectionViewDelegateIsUpdated { IGListTestAdapterDataSource *dataSource1 = [[IGListTestAdapterDataSource alloc] init]; dataSource1.objects = @[@1]; IGListAdapterUpdater *updater1 = [[IGListAdapterUpdater alloc] init]; IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:updater1 viewController:nil]; adapter1.dataSource = dataSource1; IGListTestAdapterDataSource *dataSource2 = [[IGListTestAdapterDataSource alloc] init]; dataSource1.objects = @[@1]; IGListAdapterUpdater *updater2 = [[IGListAdapterUpdater alloc] init]; IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:updater2 viewController:nil]; adapter1.dataSource = dataSource2; // associate collection view with adapter1 adapter1.collectionView = self.collectionView; XCTAssertEqual(self.collectionView.dataSource, adapter1); // associate collection view with adapter2 adapter2.collectionView = self.collectionView; XCTAssertEqual(self.collectionView.dataSource, adapter2); // associate collection view with adapter1 adapter1.collectionView = self.collectionView; XCTAssertEqual(self.collectionView.dataSource, adapter1); } - (void)test_whenCellsExtendBeyondBounds_thatVisibleSectionControllersAreLimited { // # of items for each object == [item integerValue], so @2 has 2 items (cells) self.dataSource.objects = @[@1, @2, @3, @4, @5, @6, @7, @8, @9, @10, @11, @12]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 12); NSArray *visibleSectionControllers = [self.adapter visibleSectionControllers]; // UIWindow is 100x100, each cell is 100x10 so should have the following section/cell count: 1 + 2 + 3 + 4 = 10 (100 tall) XCTAssertEqual(visibleSectionControllers.count, 4); XCTAssertTrue([visibleSectionControllers containsObject:[self.adapter sectionControllerForObject:@1]]); XCTAssertTrue([visibleSectionControllers containsObject:[self.adapter sectionControllerForObject:@2]]); XCTAssertTrue([visibleSectionControllers containsObject:[self.adapter sectionControllerForObject:@3]]); XCTAssertTrue([visibleSectionControllers containsObject:[self.adapter sectionControllerForObject:@4]]); } #if !TARGET_OS_TV - (void) test_withEmptySectionPlusFooter_thatVisibleSectionControllersAreCorrect { self.dataSource.objects = @[@0]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.dequeueFromNib = YES; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@0]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; NSArray *visibleSectionControllers = [self.adapter visibleSectionControllers]; XCTAssertTrue([visibleSectionControllers count] == 1); XCTAssertTrue(visibleSectionControllers.firstObject.supplementaryViewSource == supplementarySource); } #endif - (void)test_whenCellsExtendBeyondBounds_thatVisibleCellsExistForSectionControllers { self.dataSource.objects = @[@2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; id sectionController2 = [self.adapter sectionControllerForObject:@2]; id sectionController3 = [self.adapter sectionControllerForObject:@3]; id sectionController4 = [self.adapter sectionControllerForObject:@4]; id sectionController5 = [self.adapter sectionControllerForObject:@5]; id sectionController6 = [self.adapter sectionControllerForObject:@6]; XCTAssertEqual([self.adapter visibleCellsForSectionController:sectionController2].count, 2); XCTAssertEqual([self.adapter visibleCellsForSectionController:sectionController3].count, 3); XCTAssertEqual([self.adapter visibleCellsForSectionController:sectionController4].count, 4); XCTAssertEqual([self.adapter visibleCellsForSectionController:sectionController5].count, 1); XCTAssertEqual([self.adapter visibleCellsForSectionController:sectionController6].count, 0); } - (void)test_whenCellsExtendBeyondBounds_thatVisibleIndexPathsExistForSectionControllers { self.dataSource.objects = @[@2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; id sectionController2 = [self.adapter sectionControllerForObject:@2]; id sectionController3 = [self.adapter sectionControllerForObject:@3]; id sectionController4 = [self.adapter sectionControllerForObject:@4]; id sectionController5 = [self.adapter sectionControllerForObject:@5]; id sectionController6 = [self.adapter sectionControllerForObject:@6]; XCTAssertEqual([self.adapter visibleIndexPathsForSectionController:sectionController2].count, 2); XCTAssertEqual([self.adapter visibleIndexPathsForSectionController:sectionController3].count, 3); XCTAssertEqual([self.adapter visibleIndexPathsForSectionController:sectionController4].count, 4); XCTAssertEqual([self.adapter visibleIndexPathsForSectionController:sectionController5].count, 1); XCTAssertEqual([self.adapter visibleIndexPathsForSectionController:sectionController6].count, 0); } - (void)test_whenDataSourceAddsItems_thatEmptyViewBecomesVisible { self.dataSource.objects = @[]; UIView *background = [UIView new]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = background; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual(self.collectionView.backgroundView, background); XCTAssertFalse(self.collectionView.backgroundView.hidden); self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertTrue(self.collectionView.backgroundView.hidden); } - (void)test_whenInsertingIntoEmptySection_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertFalse(self.collectionView.backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@0]; sectionController.items = 1; [self.adapter insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; XCTAssertTrue(self.collectionView.backgroundView.hidden); } - (void)test_whenDeletingAllItemsFromSection_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; UIView *backgroundView = self.adapter.collectionView.backgroundView; XCTAssertTrue(!backgroundView || backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@1]; sectionController.items = 0; [self.adapter deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; XCTAssertFalse(self.collectionView.backgroundView.hidden); } - (void)test_whenEmptySectionAddsItems_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertFalse(self.collectionView.backgroundView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@0]; sectionController.items = 2; [self.adapter reloadSectionController:sectionController]; XCTAssertTrue(self.collectionView.backgroundView.hidden); } - (void)test_whenSectionItemsAreDeletedAsBatch_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1, @2]; ((IGListTestAdapterDataSource *)self.dataSource).backgroundView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; UIView *backgroundView = self.adapter.collectionView.backgroundView; XCTAssertTrue(!backgroundView || backgroundView.hidden); IGListTestSection *firstSectionController = [self.adapter sectionControllerForObject:@1]; IGListTestSection *secondSectionController = [self.adapter sectionControllerForObject:@2]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performBatchAnimated:YES updates:^(id batchContext) { firstSectionController.items = 0; [self.adapter deleteInSectionController:firstSectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; secondSectionController.items = 0; NSIndexSet *indexesToDelete = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]; [self.adapter deleteInSectionController:secondSectionController atIndexes:indexesToDelete]; } completion:^(BOOL finished) { XCTAssertFalse(self.collectionView.backgroundView.hidden); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenScrollViewDelegateSet_thatDelegateReceivesEvents { id mockDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; self.adapter.collectionViewDelegate = nil; self.adapter.scrollViewDelegate = mockDelegate; [[mockDelegate expect] scrollViewDidScroll:self.collectionView]; [self.adapter scrollViewDidScroll:self.collectionView]; [mockDelegate verify]; } - (void)test_whenCollectionViewDelegateSet_thatDelegateReceivesEvents { // silence display handler asserts self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; self.adapter.scrollViewDelegate = nil; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:path]; [[mockDelegate expect] collectionView:self.collectionView didEndDisplayingCell:cell forItemAtIndexPath:path]; [self.adapter collectionView:self.collectionView didEndDisplayingCell:cell forItemAtIndexPath:path]; [mockDelegate verify]; } - (void)test_whenCollectionViewDelegateSet_withScrollViewDelegateSet_thatDelegatesReceiveUniqueEvents { // silence display handler asserts self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockCollectionViewDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; id mockScrollViewDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; self.adapter.collectionViewDelegate = mockCollectionViewDelegate; self.adapter.scrollViewDelegate = mockScrollViewDelegate; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:path]; [[mockScrollViewDelegate expect] scrollViewDidScroll:self.collectionView]; [[mockCollectionViewDelegate reject] scrollViewDidScroll:self.collectionView]; [[mockCollectionViewDelegate expect] collectionView:self.collectionView didEndDisplayingCell:cell forItemAtIndexPath:path]; [self.adapter scrollViewDidScroll:self.collectionView]; [self.adapter collectionView:self.collectionView didEndDisplayingCell:cell forItemAtIndexPath:path]; [mockScrollViewDelegate verify]; [mockCollectionViewDelegate verify]; } - (void)test_whenSupplementarySourceSupportsFooter_thatHeaderViewsAreNil { self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); } #if !TARGET_OS_TV - (void)test_whenSupplementarySourceSupportsFooter_withNibs_thatHeaderViewsAreNil { self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.dequeueFromNib = YES; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; id view = [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue([view isKindOfClass:IGTestNibSupplementaryView.class]); XCTAssertEqualObjects([[(IGTestNibSupplementaryView *)view label] text], @"Foo bar baz"); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]); } #endif - (void)test_whenAdapterReleased_withSectionControllerStrongRefToCell_thatSectionControllersRelease { __weak id weakCollectionView = nil, weakAdapter = nil, weakSectionController = nil; @autoreleasepool { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; weakCollectionView = collectionView; IGListTestAdapterDataSource *dataSource = [[IGListTestAdapterDataSource alloc] init]; dataSource.objects = @[@0, @1, @2]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; weakAdapter = adapter; IGListSectionController *sectionController = [adapter sectionControllerForObject:@1]; weakSectionController = sectionController; // force the collection view to layout and generate cells [collectionView layoutIfNeeded]; UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; XCTAssertNotNil(cell); // strongly attach the cell to an section controller objc_setAssociatedObject(sectionController, @"some_random_key", cell, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // weak refs should exist at this point XCTAssertNotNil(weakCollectionView); XCTAssertNotNil(weakAdapter); XCTAssertNotNil(weakSectionController); } XCTAssertNil(weakCollectionView); XCTAssertNil(weakAdapter); XCTAssertNil(weakSectionController); } - (void)test_whenAdapterReleased_withSectionControllerStrongRefToCollectionView_thatSectionControllersRelease { __weak id weakCollectionView = nil, weakAdapter = nil, weakSectionController = nil; @autoreleasepool { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; weakCollectionView = collectionView; IGListTestAdapterDataSource *dataSource = [[IGListTestAdapterDataSource alloc] init]; dataSource.objects = @[@0, @1, @2]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; weakAdapter = adapter; IGListSectionController *sectionController = [adapter sectionControllerForObject:@1]; weakSectionController = sectionController; // force the collection view to layout and generate cells [collectionView layoutIfNeeded]; // strongly attach the cell to an section controller objc_setAssociatedObject(sectionController, @"some_random_key", collectionView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // weak refs should exist at this point XCTAssertNotNil(weakCollectionView); XCTAssertNotNil(weakAdapter); XCTAssertNotNil(weakSectionController); } XCTAssertNil(weakCollectionView); XCTAssertNil(weakAdapter); XCTAssertNil(weakSectionController); } - (void)test_whenAdapterUpdatedTwice_withThreeSections_thatSectionsUpdatedFirstLast { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertTrue([[self.adapter sectionControllerForObject:@0] isFirstSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@1] isFirstSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@2] isFirstSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@0] isLastSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@1] isLastSection]); XCTAssertTrue([[self.adapter sectionControllerForObject:@2] isLastSection]); // update and shift objects to test that first/last flags are also updated self.dataSource.objects = @[@2, @0, @1]; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertFalse([[self.adapter sectionControllerForObject:@0] isFirstSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@1] isFirstSection]); XCTAssertTrue([[self.adapter sectionControllerForObject:@2] isFirstSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@0] isLastSection]); XCTAssertTrue([[self.adapter sectionControllerForObject:@1] isLastSection]); XCTAssertFalse([[self.adapter sectionControllerForObject:@2] isLastSection]); } - (void)test_whenAdapterUpdated_withObjectsOverflow_thatVisibleObjectsIsSubsetOfAllObjects { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentOffset = CGPointMake(0, 30); [self.collectionView layoutIfNeeded]; NSArray *visibleObjects = [[self.adapter visibleObjects] sortedArrayUsingSelector:@selector(compare:)]; NSArray *expectedObjects = @[@3, @4, @5]; XCTAssertEqualObjects(visibleObjects, expectedObjects); } - (void)test_whenAdapterUpdated_withObjectsOverflow_thatIndexesOfVisibleObjectsIsCorrect { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentOffset = CGPointMake(0, 30); [self.collectionView layoutIfNeeded]; NSIndexSet *visibleIndexes = [self.adapter indexesOfVisibleObjects]; // Objects @3, @4, @5 are visible, which are at indexes 2, 3, 4 NSMutableIndexSet *expectedIndexes = [NSMutableIndexSet indexSet]; [expectedIndexes addIndex:2]; [expectedIndexes addIndex:3]; [expectedIndexes addIndex:4]; XCTAssertEqualObjects(visibleIndexes, expectedIndexes); } - (void)test_whenAdapterUpdated_thatLayoutAttributesForItemAtIndexIsCorrect { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@2, @3]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@2]; UICollectionViewLayoutAttributes *attributes = [self.adapter layoutAttributesForItemAtIndex:0 sectionController:controller]; XCTAssertNotNil(attributes); XCTAssertEqual(attributes.indexPath.section, 0); XCTAssertEqual(attributes.indexPath.item, 0); } - (void)test_whenAdapterUpdated_thatIndexPathForItemAtPointIsCorrect { // each section controller returns n items sized 100x10 // @2 has 2 items (y=0-20), @3 has 3 items (y=20-50) self.dataSource.objects = @[@2, @3]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; // Point at (50, 5) should be in section 0, item 0 (y=0-10) NSIndexPath *indexPath = [self.adapter indexPathForItemAtPoint:CGPointMake(50, 5)]; XCTAssertNotNil(indexPath); XCTAssertEqual(indexPath.section, 0); XCTAssertEqual(indexPath.item, 0); // Point at (50, 15) should be in section 0, item 1 (y=10-20) NSIndexPath *indexPath2 = [self.adapter indexPathForItemAtPoint:CGPointMake(50, 15)]; XCTAssertNotNil(indexPath2); XCTAssertEqual(indexPath2.section, 0); XCTAssertEqual(indexPath2.item, 1); } - (void)test_whenAdapterUpdated_thatConvertPointFromViewIsCorrect { self.dataSource.objects = @[@1]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; // Create a subview offset from the collection view UIView *subview = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 50, 50)]; [self.collectionView addSubview:subview]; // Point (0, 0) in subview should convert to (10, 20) in collection view CGPoint convertedPoint = [self.adapter convertPoint:CGPointZero fromView:subview]; XCTAssertEqual(convertedPoint.x, 10); XCTAssertEqual(convertedPoint.y, 20); } - (void)test_whenAdapterUpdated_fetchingCellIsValid { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@2]; UICollectionViewCell *cell = [self.adapter cellForItemAtIndex:0 sectionController:controller]; XCTAssertNotNil(cell); } - (void)test_whenAdapterUpdated_thatVisibleCellsForObjectAreFound { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@2, @10, @5]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentOffset = CGPointMake(0, 80); [self.collectionView layoutIfNeeded]; UICollectionView *collectionView = self.collectionView; NSArray *visibleCellsForObject = [[self.adapter visibleCellsForObject:@10] sortedArrayUsingComparator:^NSComparisonResult(UICollectionViewCell* lhs, UICollectionViewCell* rhs) { NSIndexPath *lhsIndexPath = [collectionView indexPathForCell:lhs]; NSIndexPath *rhsIndexPath = [collectionView indexPathForCell:rhs]; if (lhsIndexPath.section == rhsIndexPath.section) { return lhsIndexPath.item > rhsIndexPath.item; } return lhsIndexPath.section > rhsIndexPath.section; }]; XCTAssertEqual(visibleCellsForObject.count, 4); XCTAssertEqual([self.collectionView indexPathForCell:visibleCellsForObject[0]].item, 6); XCTAssertEqual([self.collectionView indexPathForCell:visibleCellsForObject[1]].item, 7); XCTAssertEqual([self.collectionView indexPathForCell:visibleCellsForObject[2]].item, 8); XCTAssertEqual([self.collectionView indexPathForCell:visibleCellsForObject[3]].item, 9); NSArray *visibleCellsForObjectTwo = [self.adapter visibleCellsForObject:@5]; XCTAssertEqual(visibleCellsForObjectTwo.count, 5); } - (void)test_whenAdapterUpdated_thatVisibleCellsForNilObjectIsEmpty { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@2, @10, @5]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentOffset = CGPointMake(0, 80); [self.collectionView layoutIfNeeded]; NSArray *visibleCellsForObject = [self.adapter visibleCellsForObject:@3]; XCTAssertEqual(visibleCellsForObject.count, 0); } - (void)test_whenAdapterUpdated_thatFullyVisibleCellsIsCorrect { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@2, @10, @5]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; // each row is 10 pixels high, and the first object has 2 rows. // the window is 100 pixels highm so the number of visible cells in the second section should be 8 IGListSectionController *controller = [self.adapter sectionControllerForObject:@10]; NSArray *visibleCells = [self.adapter fullyVisibleCellsForSectionController:controller]; XCTAssertEqual(visibleCells.count, 8); } - (void)test_whenAdapterUpdated_thatFullyVisibleIndexPathsIsCorrect { // each section controller returns n items sized 100x10 self.dataSource.objects = @[@2, @10, @5]; [self.adapter reloadDataWithCompletion:nil]; [self.collectionView layoutIfNeeded]; // each row is 10 pixels high, and the first object has 2 rows. // the window is 100 pixels highm so the number of visible cells in the second section should be 8 IGListSectionController *controller = [self.adapter sectionControllerForObject:@10]; NSArray *visibleCells = [self.adapter visibleIndexPathsForSectionController:controller]; XCTAssertEqual(visibleCells.count, 8); } - (void)test_whenScrollVerticallyToItem { // # of items for each object == [item integerValue], so @2 has 2 items (cells) self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@2 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 10); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 30); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content height minus collection view height is 110, can't scroll more than that IGAssertEqualPoint([self.collectionView contentOffset], 0, 110); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionCenteredVertically additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 110); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 110); } - (void)test_whenScrollVerticallyToItemInASectionWithNoCellsAndNoSupplymentaryView { self.dataSource.objects = @[@1, @0, @300]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 3); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@0 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@300 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 10); } - (void)test_whenScrollVerticallyToItemInASectionWithNoCellsButAHeaderSupplymentaryView { self.dataSource.objects = @[@1, @0, @300]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@0]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 3); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@0 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@0 supplementaryKinds:@[UICollectionElementKindSectionHeader] scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionTop additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 10); [self.adapter scrollToObject:@300 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 20); } - (void)test_whenScrollVerticallyToItemWithPositionning { self.dataSource.objects = @[@1, @100, @200]; [self.adapter reloadDataWithCompletion:nil]; [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionTop additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionCenteredVertically additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 10); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionTop additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 10); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionCenteredVertically additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 460); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 910); [self.adapter scrollToObject:@200 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, self.collectionView.contentSize.height - self.collectionView.frame.size.height); } - (void)test_whenScrollVerticallyToBottom_withContentInsets_thatBottomFlushWithCollectionViewBounds { self.dataSource.objects = @[@100]; [self.adapter reloadDataWithCompletion:nil]; // no insets self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 900); // top 100 self.collectionView.contentInset = UIEdgeInsetsMake(100, 0, 0, 0); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 900); // bottom 100 self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 100, 0); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 900 + 100); // top 50, bottom 100 self.collectionView.contentInset = UIEdgeInsetsMake(50, 0, 100, 0); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionBottom additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 900 + 100); } - (void)test_whenScrollHorizontalToRight_withContentInsets_thatRightFlushWithCollectionViewBounds { self.dataSource.objects = @[@100]; [self.adapter reloadDataWithCompletion:nil]; UICollectionViewFlowLayout *layout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout; layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; [layout invalidateLayout]; // no insets self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionRight additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 900, 0); // left 100 self.collectionView.contentInset = UIEdgeInsetsMake(0, 100, 0, 0); [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionRight additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 900, 0); // right 100 self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, 100); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionRight additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 900 + 100, 0); // left 50, right 100 self.collectionView.contentInset = UIEdgeInsetsMake(0, 50, 0, 100); [self.collectionView layoutIfNeeded]; [self.adapter scrollToObject:@100 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionRight additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 900 + 100, 0); } - (void)test_whenScrollHorizontallyToItem { // # of items for each object == [item integerValue], so @2 has 2 items (cells) IGListTestAdapterHorizontalDataSource *dataSource = [[IGListTestAdapterHorizontalDataSource alloc] init]; self.adapter.dataSource = dataSource; dataSource.objects = @[@1, @2, @3, @4, @5, @6]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionLeft additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@2 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 10, 0); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 30, 0); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content width minus collection view width is 110, can't scroll more than that IGAssertEqualPoint([self.collectionView contentOffset], 110, 0); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionCenteredHorizontally additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 110, 0); [self.adapter scrollToObject:@6 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionRight additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 110, 0); self.layout.scrollDirection = UICollectionViewScrollDirectionVertical; self.adapter.dataSource = self.dataSource; } - (void)test_whenScrollToItem_thatSupplementarySourceSupportsSingleHeader { self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); [self.adapter scrollToObject:@1 supplementaryKinds:@[UICollectionElementKindSectionHeader] scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@2 supplementaryKinds:@[UICollectionElementKindSectionHeader] scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content height smaller than collection view height, won't scroll IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); } - (void)test_whenScrollToItem_thatSupplementarySourceSupportsHeaderAndFooter { self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader, UICollectionElementKindSectionFooter]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); [self.adapter scrollToObject:@1 supplementaryKinds:@[UICollectionElementKindSectionHeader, UICollectionElementKindSectionFooter] scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@2 supplementaryKinds:@[UICollectionElementKindSectionHeader, UICollectionElementKindSectionFooter] scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content height smaller than collection view height, won't scroll IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); } - (void)test_whenScrollVerticallyToItem_thatFeedIsEmpty { self.dataSource.objects = @[]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); } - (void)test_whenScrollVerticallyToItem_thatItemNotInFeed { // # of items for each object == [item integerValue], so @2 has 2 items (cells) self.dataSource.objects = @[@1, @2, @3, @4]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 4); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@5 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@2 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content height is smaller than collection view height, can't scroll IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); [self.adapter scrollToObject:@5 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; // Content height is smaller than collection view height, can't scroll IGAssertEqualPoint([self.collectionView contentOffset], 0, 0); } - (void)test_whenScrollToItem_thatNonVisibleCellsDidNotAppear { // Regenerate the source with existing objects self.dataSource = [IGListTestAdapterDataSource new]; self.dataSource.objects = @[@20, @22]; self.adapter.dataSource = self.dataSource; // # of items for each object == [item integerValue], so @2 has 2 items (cells) // Assumptions: UICollectionView size is (100,100), each cell size is (100,10) [self.adapter scrollToObject:@22 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionTop additionalOffset:0 animated:NO]; // Force the layout, which creates the cells [self.collectionView layoutIfNeeded]; IGListTestSection *firstSection = [self.adapter sectionControllerForObject:@20]; XCTAssertNotNil(firstSection); IGListTestSection *lastSection = [self.adapter sectionControllerForObject:@22]; XCTAssertNotNil(lastSection); XCTAssertFalse(firstSection.wasDisplayed); XCTAssertTrue(lastSection.wasDisplayed); } - (void)test_whenScrolledVertically_thatIndexPathForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 3); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(0, 8) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(0, 12) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:1]); [self.collectionView setContentOffset:CGPointMake(0, 35) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:2]); [self.collectionView setContentOffset:CGPointMake(0, 40) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:1 inSection:2]); [self.collectionView setContentOffset:CGPointMake(0, 59) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:2 inSection:2]); } - (void)test_whenScrolledVerticallyWithContentInset_thatIndexPathForFirstVisibleItemIsCorrect { const CGFloat inset = 5; self.collectionView.contentInset = UIEdgeInsetsMake(inset, 0, inset, 0); self.dataSource.objects = @[@1, @2, @3]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 3); [self.collectionView setContentOffset:CGPointMake(0, 0 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(0, 8 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(0, 12 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:1]); [self.collectionView setContentOffset:CGPointMake(0, 35 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:2]); [self.collectionView setContentOffset:CGPointMake(0, 40 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:1 inSection:2]); [self.collectionView setContentOffset:CGPointMake(0, 59 - inset) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:2 inSection:2]); } - (void)test_whenScrolledHorizontallyWithSingleRow_thatIndexPathForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3, @4]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.collectionView.frame = CGRectMake(0, 0, 300, 10); [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 4); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(80, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(120, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:1]); [self.collectionView setContentOffset:CGPointMake(350, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:2]); [self.collectionView setContentOffset:CGPointMake(400, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:1 inSection:2]); [self.collectionView setContentOffset:CGPointMake(590, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:2 inSection:2]); } - (void)test_whenScrolledHorizontallyWithSingleRowWithContentInset_thatIndexPathForFirstVisibleItemIsCorrect { const CGFloat inset = 172; self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset); self.dataSource.objects = @[@1, @2, @3, @4]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.collectionView.frame = CGRectMake(0, 0, 300, 10); [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 4); [self.collectionView setContentOffset:CGPointMake(0 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(80 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(120 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:1]); [self.collectionView setContentOffset:CGPointMake(350 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:2]); [self.collectionView setContentOffset:CGPointMake(400 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:1 inSection:2]); [self.collectionView setContentOffset:CGPointMake(590 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:2 inSection:2]); } - (void)test_whenScrolledHorizontallyWithStackedSections_thatIndexPathForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(80, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:0]); [self.collectionView setContentOffset:CGPointMake(120, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:1]); [self.collectionView setContentOffset:CGPointMake(350, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:3]); [self.collectionView setContentOffset:CGPointMake(400, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:4]); [self.collectionView setContentOffset:CGPointMake(590, 0) animated:NO]; XCTAssertEqual([self.adapter indexPathForFirstVisibleItem], [NSIndexPath indexPathForItem:0 inSection:5]); } - (void)test_whenScrolledVertically_thatOffsetForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.collectionView setContentOffset:CGPointMake(0, 8) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 8); [self.collectionView setContentOffset:CGPointMake(0, 12) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 2); [self.collectionView setContentOffset:CGPointMake(0, 35) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 5); [self.collectionView setContentOffset:CGPointMake(0, 40) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.collectionView setContentOffset:CGPointMake(0, 59) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 9); } - (void)test_whenScrolledVerticallyWithContentInset_thatOffsetForFirstVisibleItemIsCorrect { const CGFloat inset = 5; self.collectionView.contentInset = UIEdgeInsetsMake(inset, 0, inset, 0); self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.collectionView setContentOffset:CGPointMake(0, 0 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.collectionView setContentOffset:CGPointMake(0, 8 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 8); [self.collectionView setContentOffset:CGPointMake(0, 12 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 2); [self.collectionView setContentOffset:CGPointMake(0, 35 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 5); [self.collectionView setContentOffset:CGPointMake(0, 40 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.collectionView setContentOffset:CGPointMake(0, 59 - inset) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 9); } - (void)test_whenScrolledHorizontallyWithSingleRow_thatOffsetForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3, @4]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.collectionView.frame = CGRectMake(0, 0, 300, 10); XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 4); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(80, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 80); [self.collectionView setContentOffset:CGPointMake(120, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 20); [self.collectionView setContentOffset:CGPointMake(350, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 50); [self.collectionView setContentOffset:CGPointMake(400, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(599, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 99); } - (void)test_whenScrolledHorizontallyWithSingleRowWithContentInset_thatOffsetForFirstVisibleItemIsCorrect { const CGFloat inset = 172; self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset); self.dataSource.objects = @[@1, @2, @3, @4]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.collectionView.frame = CGRectMake(0, 0, 300, 10); [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 4); [self.collectionView setContentOffset:CGPointMake(0 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(80 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 80); [self.collectionView setContentOffset:CGPointMake(120 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 20); [self.collectionView setContentOffset:CGPointMake(350 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 50); [self.collectionView setContentOffset:CGPointMake(400 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(599 - inset, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 99); } - (void)test_whenScrolledHorizontallyWithStackedSections_thatOffsetForFirstVisibleItemIsCorrect { self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.collectionView setContentOffset:CGPointZero animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(80, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 80); [self.collectionView setContentOffset:CGPointMake(120, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 20); [self.collectionView setContentOffset:CGPointMake(350, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 50); [self.collectionView setContentOffset:CGPointMake(400, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.collectionView setContentOffset:CGPointMake(599, 0) animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 99); } - (void)test_whenScrollToIndexPathWithAdditionalOffsetVerticially_thatOffsetForFirstVisibleItemIsCorrect { const CGFloat inset = 5; self.collectionView.contentInset = UIEdgeInsetsMake(inset, 0, inset, 0); self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:7 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 7); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 0); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:3 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 3); [self.adapter scrollToObject:@4 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionVertical scrollPosition:UICollectionViewScrollPositionNone additionalOffset:9 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionVertical], 9); } - (void)test_whenScrollToIndexPathWithAdditionalOffsetHorizontally_thatOffsetForFirstVisibleItemIsCorrect { const CGFloat inset = 172; self.collectionView.contentInset = UIEdgeInsetsMake(0, inset, 0, inset); self.dataSource.objects = @[@1, @2, @3, @4, @5, @6]; self.layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfSections], 6); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.adapter scrollToObject:@1 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:73 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 73); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:0 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 0); [self.adapter scrollToObject:@3 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:36 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 36); [self.adapter scrollToObject:@4 supplementaryKinds:nil scrollDirection:UICollectionViewScrollDirectionHorizontal scrollPosition:UICollectionViewScrollPositionNone additionalOffset:99 animated:NO]; XCTAssertEqual([self.adapter offsetForFirstVisibleItemWithScrollDirection:UICollectionViewScrollDirectionHorizontal], 99); } - (void)test_whenQueryingIndexPath_withOOBSectionController_thatNilReturned { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id randomSectionController = [IGListSectionController new]; XCTAssertNil([self.adapter indexPathForSectionController:randomSectionController index:0 usePreviousIfInUpdateBlock:NO]); } - (void)test_whenQueryingSectionForObject_thatSectionReturned { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.adapter sectionForObject:@0], 0); XCTAssertEqual([self.adapter sectionForObject:@1], 1); XCTAssertEqual([self.adapter sectionForObject:@2], 2); XCTAssertEqual([self.adapter sectionForObject:@3], NSNotFound); } - (void)test_whenQueryingSectionControllerForSection_thatControllerReturned { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.adapter sectionControllerForSection:0], [self.adapter sectionControllerForObject:@0]); XCTAssertEqual([self.adapter sectionControllerForSection:1], [self.adapter sectionControllerForObject:@1]); XCTAssertEqual([self.adapter sectionControllerForSection:2], [self.adapter sectionControllerForObject:@2]); } - (void)test_whenReloadingData_withNoDataSource_thatCompletionCalledWithNO { self.dataSource.objects = @[@1]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil]; adapter.collectionView = self.collectionView; __block BOOL executed = NO; [adapter reloadDataWithCompletion:^(BOOL finished) { executed = YES; XCTAssertFalse(finished); }]; XCTAssertTrue(executed); } - (void)test_whenReloadingData_withNoCollectionView_thatCompletionCalledWithNO { self.dataSource.objects = @[@1]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil]; adapter.dataSource = self.dataSource; __block BOOL executed = NO; [adapter reloadDataWithCompletion:^(BOOL finished) { executed = YES; XCTAssertFalse(finished); }]; XCTAssertTrue(executed); } - (void)test_whenSectionControllerDeleting_withEmptyIndexes_thatNoUpdatesHappen { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListUpdatingDelegate)]; [[mockDelegate reject] deleteItemsFromCollectionView:[OCMArg any] indexPaths:[OCMArg any]]; self.adapter.updater = mockDelegate; id sectionController = [self.adapter sectionControllerForObject:@1]; [self.adapter deleteInSectionController:sectionController atIndexes:[NSIndexSet new]]; [mockDelegate verify]; } - (void)test_whenSectionControllerInserting_withEmptyIndexes_thatNoUpdatesHappen { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListUpdatingDelegate)]; [[mockDelegate reject] insertItemsIntoCollectionView:[OCMArg any] indexPaths:[OCMArg any]]; self.adapter.updater = mockDelegate; id sectionController = [self.adapter sectionControllerForObject:@1]; [self.adapter insertInSectionController:sectionController atIndexes:[NSIndexSet new]]; [mockDelegate verify]; } - (void)test_whenReloading_withSectionControllerNotFound_thatNoUpdatesHappen { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListUpdatingDelegate)]; [[mockDelegate reject] reloadCollectionView:[OCMArg any] sections:[OCMArg any]]; self.adapter.updater = mockDelegate; id sectionController = [IGListSectionController new]; [self.adapter reloadSectionController:sectionController]; [mockDelegate verify]; } - (void)test_whenSelectingCell_thatCollectionViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView didSelectItemAtIndexPath:indexPath]; // simulates the collection view telling its delegate that it was tapped [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:indexPath]; [mockDelegate verify]; } - (void)test_whenSelectingCell_thatSelectionConfirmationDelegatesReturnValues { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; XCTAssertTrue([self.adapter collectionView:self.collectionView shouldSelectItemAtIndexPath:indexPath]); XCTAssertTrue([self.adapter collectionView:self.collectionView shouldDeselectItemAtIndexPath:indexPath]); } - (void)test_whenSelectingCell_thatSectionControllerReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // simulates the collectionview telling its delegate that it was tapped [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:indexPath]; IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; XCTAssertTrue(s0.wasSelected); XCTAssertFalse(s1.wasSelected); XCTAssertFalse(s2.wasSelected); } - (void)test_whenSelectingCell_withAutoDeselectEnabled_thatCellIsDeselected { self.dataSource.objects = @[@1, @1, @1]; [self.adapter reloadDataWithCompletion:nil]; self.adapter.autoDeselectEnabled = YES; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // Select a cell [self.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone]; // Make sure the selection worked (doesn't call delegate) XCTAssertEqual([[self.collectionView indexPathsForSelectedItems] count], 1); // Manually call delegate, which should auto-deselect [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:indexPath]; // Make sure it was really deselection on the view too XCTAssertEqual([[self.collectionView indexPathsForSelectedItems] count], 0); } - (void)test_whenDeselectingCell_thatCollectionViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView didDeselectItemAtIndexPath:indexPath]; // simulates the collectionview telling its delegate that it was tapped [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:indexPath]; [mockDelegate verify]; } - (void)test_whenDeselectingCell_thatSectionControllerReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // simulates the collectionview telling its delegate that it was tapped [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:indexPath]; IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; XCTAssertTrue(s0.wasDeselected); XCTAssertFalse(s1.wasDeselected); XCTAssertFalse(s2.wasDeselected); } - (void)test_whenDisplayingCell_thatCollectionViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [UICollectionViewCell new]; [[mockDelegate expect] collectionView:self.collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; // simulates the collectionview telling its delegate that a cell will be displayed [self.adapter collectionView:self.collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; [mockDelegate verify]; } - (void)test_whenWillBeginDragging_thatScrollViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockCollectionDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; self.adapter.collectionViewDelegate = mockCollectionDelegate; self.adapter.scrollViewDelegate = mockScrollDelegate; [[mockCollectionDelegate reject] scrollViewWillBeginDragging:self.collectionView]; [[mockScrollDelegate expect] scrollViewWillBeginDragging:self.collectionView]; // simulates the scrollview delegate telling the adapter that it will begin dragging [self.adapter scrollViewWillBeginDragging:self.collectionView]; [mockCollectionDelegate verify]; [mockScrollDelegate verify]; } - (void)test_whenDidEndDragging_thatScrollViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockCollectionDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; self.adapter.collectionViewDelegate = mockCollectionDelegate; self.adapter.scrollViewDelegate = mockScrollDelegate; [[mockCollectionDelegate reject] scrollViewDidEndDragging:self.collectionView willDecelerate:NO]; [[mockScrollDelegate expect] scrollViewDidEndDragging:self.collectionView willDecelerate:NO]; // simulates the scrollview delegate telling the adapter that it will end dragging [self.adapter scrollViewDidEndDragging:self.collectionView willDecelerate:NO]; [mockCollectionDelegate verify]; [mockScrollDelegate verify]; } - (void)test_whenDidEndDecelerating_thatScrollViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockCollectionDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; id mockScrollDelegate = [OCMockObject mockForProtocol:@protocol(UIScrollViewDelegate)]; self.adapter.collectionViewDelegate = mockCollectionDelegate; self.adapter.scrollViewDelegate = mockScrollDelegate; id mockSectionControllerScrollDelegate = [OCMockObject mockForProtocol:@protocol(IGListScrollDelegate)]; IGListSectionController *firstController = [self.adapter.visibleSectionControllers firstObject]; firstController.scrollDelegate = mockSectionControllerScrollDelegate; [[mockCollectionDelegate reject] scrollViewDidEndDecelerating:self.collectionView]; [[mockScrollDelegate expect] scrollViewDidEndDecelerating:self.collectionView]; [[mockSectionControllerScrollDelegate expect] listAdapter:self.adapter didEndDeceleratingSectionController:firstController]; // simulates the scrollview delegate telling the adapter that it ended decelerating [self.adapter scrollViewDidEndDecelerating:self.collectionView]; [mockCollectionDelegate verify]; [mockScrollDelegate verify]; [mockSectionControllerScrollDelegate verify]; } - (void)test_whenReloadingObjectsThatDontExist_thatAdapterContinues { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; [self.adapter reloadObjects:@[@1, @3]]; XCTAssertEqual(self.collectionView.numberOfSections, 3); } - (void)test_whenDeselectingThroughContext_thatCellDeselected { self.dataSource.objects = @[@1, @2, @3]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; [self.collectionView selectItemAtIndexPath:path animated:NO scrollPosition:UICollectionViewScrollPositionTop]; XCTAssertTrue([[self.collectionView cellForItemAtIndexPath:path] isSelected]); id section = [self.adapter sectionControllerForObject:@1]; [self.adapter deselectItemAtIndex:0 sectionController:section animated:NO]; XCTAssertFalse([[self.collectionView cellForItemAtIndexPath:path] isSelected]); } - (void)test_whenSelectingThroughContext_thatCellSelected { self.dataSource.objects = @[@1, @2, @3]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; [self.adapter selectItemAtIndex:0 sectionController:[self.adapter sectionControllerForObject:@1] animated:NO scrollPosition:UICollectionViewScrollPositionTop]; XCTAssertTrue([[self.collectionView cellForItemAtIndexPath:path] isSelected]); } - (void)test_whenScrollingToIndex_withSectionController_thatPositionCorrect { self.dataSource.objects = @[@1, @2, @3, @4, @5, @6, @7, @8, @9, @10, @11, @12, @13, @14, @15, @16, @17, @18, @19]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *section = [self.adapter sectionControllerForObject:@8]; [section.collectionContext scrollToSectionController:section atIndex:0 scrollPosition:UICollectionViewScrollPositionTop animated:NO]; XCTAssertEqual(self.collectionView.contentOffset.x, 0); XCTAssertEqual(self.collectionView.contentOffset.y, 280); } - (void)test_whenDisplayingSectionController_withOnlySupplementaryView_thatDisplayEventStillSent { self.dataSource.objects = @[@0]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@0]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; id mockDisplayDelegate = [OCMockObject mockForProtocol:@protocol(IGListDisplayDelegate)]; [[mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:controller]; [[mockDisplayDelegate reject] listAdapter:self.adapter willDisplaySectionController:controller cell:[OCMArg any] atIndex:0]; controller.displayDelegate = mockDisplayDelegate; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); [mockDisplayDelegate verify]; } - (void)test_whenDisplayingSectionController_fetchingCellIndexIsValid { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; UICollectionViewCell *cell = [self.adapter cellForItemAtIndex:0 sectionController:controller]; XCTAssertNotNil(cell); NSInteger index = [self.adapter indexForCell:cell sectionController:controller]; XCTAssertNotEqual(index, NSNotFound); XCTAssertEqual(index, 0); } - (void)test_whenEndingDisplayOfSectionController_withOnlySupplementaryView_thatDisplayEventStillSent { self.dataSource.objects = @[@0]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@0]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); id mockDisplayDelegate = [OCMockObject mockForProtocol:@protocol(IGListDisplayDelegate)]; [[mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:controller]; [[mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:controller cell:[OCMArg any] atIndex:0]; controller.displayDelegate = mockDisplayDelegate; controller.supplementaryViewSource = nil; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNil([self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]); [mockDisplayDelegate verify]; } - (void)test_whenWillDisplaySupplementaryView_thatCollectionViewDelegateReceivesEvents { // silence display handler asserts self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; UICollectionReusableView *view = [UICollectionReusableView new]; NSString *kind = @"kind"; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView willDisplaySupplementaryView:view forElementKind:kind atIndexPath:path]; [self.adapter collectionView:self.collectionView willDisplaySupplementaryView:view forElementKind:kind atIndexPath:path]; [mockDelegate verify]; } - (void)test_whenEndDisplayingSupplementaryView_thatCollectionViewDelegateReceivesEvents { // silence display handler asserts self.dataSource.objects = @[@1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; UICollectionReusableView *view = [UICollectionReusableView new]; NSString *kind = @"kind"; NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView didEndDisplayingSupplementaryView:view forElementOfKind:kind atIndexPath:path]; [self.adapter collectionView:self.collectionView didEndDisplayingSupplementaryView:view forElementOfKind:kind atIndexPath:path]; [mockDelegate verify]; } - (void)test_whenHighlightingCell_thatCollectionViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; // simulates the collectionview telling its delegate that it was highlighted [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; [mockDelegate verify]; } - (void)test_whenHighlightingCell_thatSectionControllerReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // simulates the collectionview telling its delegate that it was highlighted [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:indexPath]; IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; XCTAssertTrue(s0.wasHighlighted); XCTAssertFalse(s1.wasHighlighted); XCTAssertFalse(s2.wasHighlighted); } - (void)test_whenUnhighlightingCell_thatCollectionViewDelegateReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; // simulates the collectionview telling its delegate that it was unhighlighted [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; [mockDelegate verify]; } - (void)test_whenUnhighlightingCell_thatSectionControllerReceivesMethod { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // simulates the collectionview telling its delegate that it was unhighlighted [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:indexPath]; IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; XCTAssertTrue(s0.wasUnhighlighted); XCTAssertFalse(s1.wasUnhighlighted); XCTAssertFalse(s2.wasUnhighlighted); } #if !TARGET_OS_TV - (void)test_whenContextMenuAskedCell_thatCollectionViewDelegateReceivesMethod API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; self.adapter.collectionViewDelegate = mockDelegate; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [[mockDelegate expect] collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero]; // simulates the collectionview telling its delegate that it needs the context menu configuration [self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero]; [mockDelegate verify]; } - (void)test_whenContextMenuAskedCell_thatSectionControllerReceivesMethod API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:nil]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; // simulates the collectionview telling its delegate that it needs the context menu configuration [self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:indexPath point:CGPointZero]; IGListTestSection *s0 = [self.adapter sectionControllerForObject:@0]; IGListTestSection *s1 = [self.adapter sectionControllerForObject:@1]; IGListTestSection *s2 = [self.adapter sectionControllerForObject:@2]; XCTAssertTrue(s0.requestedContextMenu); XCTAssertFalse(s1.requestedContextMenu); XCTAssertFalse(s2.requestedContextMenu); } #endif - (void)test_whenDataSourceDoesntHandleObject_thatObjectIsDropped { // IGListTestAdapterDataSource does not handle NSStrings self.dataSource.objects = @[@1, @"dog", @2]; [self.adapter reloadDataWithCompletion:nil]; NSArray *expected = @[@1, @2]; XCTAssertEqualObjects(self.adapter.objects, expected); } - (void)test_whenSectionEdgeInsetIsNotZero { // IGListTestAdapterDataSource does not handle NSStrings self.dataSource.objects = @[@42]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@42]; IGAssertEqualSize([self.adapter containerSizeForSectionController:controller], 98, 98); } - (void)test_whenSectionControllerReturnsNegativeSize_thatAdapterReturnsZero { self.dataSource.objects = @[@1]; IGListTestSection *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; section.size = CGSizeMake(-1, -1); const CGSize size = [self.adapter sizeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqual(size.width, 0.0); XCTAssertEqual(size.height, 0.0); } - (void)test_whenSupplementarySourceReturnsNegativeSize_thatAdapterReturnsZero { self.dataSource.objects = @[@1]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionFooter]; supplementarySource.size = CGSizeMake(-1, -1); IGListSectionController *controller = [self.adapter sectionControllerForObject:@1]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; const CGSize size = [self.adapter sizeForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqual(size.width, 0.0); XCTAssertEqual(size.height, 0.0); } - (void)test_whenQueryingTraitCollection_thatMatchesCollectionView { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual(self.adapter.traitCollection, self.collectionView.traitCollection); } - (void)test_whenQueryingContainerInset_thatMatchesCollectionView { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentInset = UIEdgeInsetsMake(1, 2, 3, 4); IGListSectionController *controller = [self.adapter sectionControllerForObject:@2]; const UIEdgeInsets inset = [controller.collectionContext containerInset]; XCTAssertEqual(inset.top, 1); XCTAssertEqual(inset.left, 2); XCTAssertEqual(inset.bottom, 3); XCTAssertEqual(inset.right, 4); } - (void)test_whenQueryingInsetContainerSize_thatResultIsBoundsInsetByContent { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; self.collectionView.contentInset = UIEdgeInsetsMake(1, 2, 3, 4); IGListSectionController *controller = [self.adapter sectionControllerForObject:@2]; const CGSize size = [controller.collectionContext insetContainerSize]; XCTAssertEqual(size.width, 94); XCTAssertEqual(size.height, 96); } - (void)test_whenQueryingContainerContentOffset_thatResultIsCorrectlyOffset { self.dataSource.objects = @[@1, @2, @3, @4, @5, @6, @7, @8, @9, @10]; [self.adapter reloadDataWithCompletion:nil]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@3]; [self.adapter scrollToSectionController:controller atIndex:0 scrollPosition:UICollectionViewScrollPositionTop animated:NO]; const CGPoint offset = [controller.collectionContext containerContentOffset]; XCTAssertEqual(offset.x, 0); XCTAssertEqual(offset.y, 30); } - (void)test_whenInsertingAtBeginning_thatAllSectionControllerIndexesUpdateCorrectly_forInsertAtHead { NSNumber *zero = @0; NSNumber *one = @1; NSNumber *two = @2; NSNumber *three = @3; self.dataSource.objects = @[one, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *controller1a = [self.adapter sectionControllerForObject:one]; XCTAssertEqual(controller1a.section, 0); XCTAssertTrue(controller1a.isFirstSection); XCTAssertEqual([self.adapter sectionControllerForObject:two].section, 1); XCTAssertEqual([self.adapter sectionControllerForObject:three].section, 2); XCTAssertTrue([self.adapter sectionControllerForObject:three].isLastSection); self.dataSource.objects = @[zero, one, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *controller0 = [self.adapter sectionControllerForObject:zero]; XCTAssertEqual(controller0.section, 0); XCTAssertTrue(controller0.isFirstSection); IGListSectionController *controller1b = [self.adapter sectionControllerForObject:one]; XCTAssertEqual(controller1b.section, 1); XCTAssertFalse(controller1b.isFirstSection); XCTAssertEqual([self.adapter sectionControllerForObject:two].section, 2); XCTAssertEqual([self.adapter sectionControllerForObject:three].section, 3); XCTAssertTrue([self.adapter sectionControllerForObject:three].isLastSection); } - (void)test_whenRemovingFromHead_thatAllSectionControllerIndexesUpdateCorrectly_RemovedSectionControllerIsNotFound { NSNumber *zero = @0; NSNumber *one = @1; NSNumber *two = @2; NSNumber *three = @3; self.dataSource.objects = @[zero, one, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *zeroController = [self.adapter sectionControllerForSection:0]; XCTAssertEqual(zeroController.section, 0); XCTAssertTrue(zeroController.isFirstSection); IGListSectionController *oneController = [self.adapter sectionControllerForSection:1]; XCTAssertEqual(oneController.section, 1); XCTAssertFalse(oneController.isFirstSection); IGListSectionController *threeController = [self.adapter sectionControllerForSection:3]; XCTAssertEqual(threeController.section, 3); XCTAssertTrue(threeController.isLastSection); self.dataSource.objects = @[one, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertEqual(zeroController.section, NSNotFound); XCTAssertFalse(zeroController.isFirstSection); XCTAssertEqual(oneController.section, 0); XCTAssertTrue(oneController.isFirstSection); XCTAssertEqual(threeController.section, 2); XCTAssertTrue(threeController.isLastSection); } - (void)test_whenRemovingFromMiddle_thatAllSectionControllerIndexesUpdateCorrectly_removedSectionControllerIsNotFound { NSNumber *zero = @0; NSNumber *one = @1; NSNumber *two = @2; NSNumber *three = @3; self.dataSource.objects = @[zero, one, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *zeroController = [self.adapter sectionControllerForSection:0]; XCTAssertEqual(zeroController.section, 0); XCTAssertTrue(zeroController.isFirstSection); IGListSectionController *oneController = [self.adapter sectionControllerForSection:1]; XCTAssertEqual(oneController.section, 1); XCTAssertFalse(oneController.isFirstSection); IGListSectionController *threeController = [self.adapter sectionControllerForSection:3]; XCTAssertEqual(threeController.section, 3); XCTAssertTrue(threeController.isLastSection); self.dataSource.objects = @[zero, two, three]; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertEqual(zeroController.section, 0); XCTAssertTrue(zeroController.isFirstSection); XCTAssertEqual(oneController.section, NSNotFound); XCTAssertFalse(oneController.isFirstSection); XCTAssertEqual(threeController.section, 2); XCTAssertTrue(threeController.isLastSection); } - (void)test_withStrongRefToSectionController_thatAdaptersectionIsZero_thatSectionControllerIndexDoesNotChange { IGListSectionController *sc = nil; // hold a weak reference to simulate what would happen to the collectionContext object on a section controller // if the section controller were held strongly by an async block and the rest of the infra was deallocated __weak IGListAdapter *wAdapter = nil; @autoreleasepool { IGListTestAdapterDataSource *dataSource = [IGListTestAdapterDataSource new]; IGListReloadDataUpdater *updater = [IGListReloadDataUpdater new]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; adapter.dataSource = dataSource; adapter.collectionView = self.collectionView; wAdapter = adapter; dataSource.objects = @[@0, @1, @2]; [adapter performUpdatesAnimated:NO completion:nil]; sc = [adapter sectionControllerForSection:1]; XCTAssertEqual(sc.section, 1); } XCTAssertEqual(sc.section, NSNotFound); XCTAssertEqual([wAdapter sectionForSectionController:sc], 0); } - (void)test_whenSwappingCollectionViews_withMultipleAdapters_thatDoesntNilOtherAdaptersCollectionView { IGListTestAdapterDataSource *dataSource1 = [IGListTestAdapterDataSource new]; IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter1.dataSource = dataSource1; IGListTestAdapterDataSource *dataSource2 = [IGListTestAdapterDataSource new]; IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter2.dataSource = dataSource2; UICollectionView *collectionView1 = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; UICollectionView *collectionView2 = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; adapter1.collectionView = collectionView1; adapter2.collectionView = collectionView2; XCTAssertEqual(adapter1.collectionView, collectionView1); XCTAssertEqual(collectionView1.dataSource, adapter1); XCTAssertEqual(adapter2.collectionView, collectionView2); XCTAssertEqual(collectionView2.dataSource, adapter2); adapter2.collectionView = collectionView1; XCTAssertEqual(adapter2.collectionView, collectionView1); XCTAssertEqual(collectionView1.dataSource, adapter2); XCTAssertNil(adapter1.collectionView); adapter1.collectionView = collectionView2; XCTAssertEqual(adapter1.collectionView, collectionView2); XCTAssertEqual(collectionView2.dataSource, adapter1); XCTAssertEqual(adapter2.collectionView, collectionView1); XCTAssertEqual(collectionView1.dataSource, adapter2); } - (void)test_whenPrefetchingEnabled_thatSetterDisables { if (@available(iOS 10.0, *)) { UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:[UICollectionViewFlowLayout new]]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter.collectionView = collectionView; XCTAssertFalse(collectionView.prefetchingEnabled); } } - (void)test_whenSectionControllerReorderDisabled_thatAdapterReorderDisabled { BOOL isReorderable = NO; IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1, @2]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; IGTestReorderableSection *section = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section.isReorderable = isReorderable; [self.adapter performUpdatesAnimated:NO completion:nil]; BOOL canMove = [self.adapter collectionView:self.collectionView canMoveItemAtIndexPath:indexPath]; XCTAssertFalse(canMove); } - (void)test_whenSectionControllerReorderEnabled_thatAdapterReorderEnabled { BOOL isReorderable = YES; IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1, @2]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; IGTestReorderableSection *section = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section.isReorderable = isReorderable; [self.adapter performUpdatesAnimated:NO completion:nil]; BOOL canMove = [self.adapter collectionView:self.collectionView canMoveItemAtIndexPath:indexPath]; XCTAssertTrue(canMove); } - (NSIndexPath *)interpretedIndexPathFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *updatedIndexPath = [layout updatedTargetForInteractivelyMovingItem:fromIndexPath toIndexPath:toIndexPath adapter:self.adapter]; if (!updatedIndexPath) { return toIndexPath; } return updatedIndexPath; } - (void)test_whenSectionIsInteractivelyReordered_thatIndexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1, @2]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; IGTestReorderableSection *section0 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; IGTestReorderableSection *section1 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:1]; IGTestReorderableSection *section2 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:2]; section0.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section1.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section2.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section2.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath, *interpretedPath; // move the last section into the first position, dropping into the end of the first section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section0, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section1, [self.adapter sectionControllerForSection:2]); // move the last section into the first position, dropping into the start of the first section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section1, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section0, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:2]); // move the first section into the last position, dropping into the start of the last section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section0, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section1, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:2]); // move the first section into the last position, dropping into the end of the last section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:2]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section1, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section0, [self.adapter sectionControllerForSection:2]); } - (void)test_whenItemsInSectionAreInteractivelyReordered_thatIndexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSArray *sectionObjects = @[@0, @1, @2]; IGTestReorderableSection *section = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:sectionObjects]; section.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath; // move the last item into the first position fromIndexPath = [NSIndexPath indexPathForItem:2 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; XCTAssertEqual(sectionObjects[2], section.sectionObject.objects[0]); XCTAssertEqual(sectionObjects[0], section.sectionObject.objects[1]); XCTAssertEqual(sectionObjects[1], section.sectionObject.objects[2]); // move the last item into the middle position fromIndexPath = [NSIndexPath indexPathForItem:2 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; XCTAssertEqual(sectionObjects[2], section.sectionObject.objects[0]); XCTAssertEqual(sectionObjects[1], section.sectionObject.objects[1]); XCTAssertEqual(sectionObjects[0], section.sectionObject.objects[2]); } - (void)test_whenItemsInSectionAreInteractivelyReordered_andReorderingIsDisallowed_thatIndexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSArray *sectionObjects = @[@0, @1, @2]; IGTestReorderableSection *section = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:sectionObjects]; section.isReorderable = NO; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath; // move the last item into the first position fromIndexPath = [NSIndexPath indexPathForItem:2 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; XCTAssertEqual(sectionObjects[0], section.sectionObject.objects[0]); XCTAssertEqual(sectionObjects[1], section.sectionObject.objects[1]); XCTAssertEqual(sectionObjects[2], section.sectionObject.objects[2]); } - (void)test_whenItemsAreInteractivelyReorderedAcrossSections_thatIndexesRevertToOriginalState { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSArray *section0Objects = @[@0, @1, @2]; NSArray *section1Objects = @[@3, @4, @5]; IGTestReorderableSection *section0 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section0.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:section0Objects]; IGTestReorderableSection *section1 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:1]; section1.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:section1Objects]; section1.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath; // move an item from section 1 to section 0 and check that they are reverted fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:1]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.collectionView performBatchUpdates:^{ [self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.collectionView.dataSource collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; } completion:nil]; XCTAssertEqual(section0Objects[0], section0.sectionObject.objects[0]); XCTAssertEqual(section0Objects[1], section0.sectionObject.objects[1]); XCTAssertEqual(section0Objects[2], section0.sectionObject.objects[2]); XCTAssertEqual(section1Objects[0], section1.sectionObject.objects[0]); XCTAssertEqual(section1Objects[1], section1.sectionObject.objects[1]); XCTAssertEqual(section1Objects[2], section1.sectionObject.objects[2]); } - (void)test_whenSingleItemInSectionIsInteractivelyReorderedThorughLastSpot_indexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1, @2]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; IGTestReorderableSection *section0 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; IGTestReorderableSection *section1 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:1]; IGTestReorderableSection *section2 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:2]; section0.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section0.isReorderable = YES; section1.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section2.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *lastSpotIndexPath = [NSIndexPath indexPathForItem:1 inSection:2]; NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:1 inSection:1]; // move the first section item to the middle while simulating dragging to the last spot and back. NSIndexPath *interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:lastSpotIndexPath]; interpretedPath = [self interpretedIndexPathFromIndexPath:interpretedPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section0, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section1, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:2]); } - (void)test_whenReload_thatPerformanceDelegateEventsSent { self.dataSource.objects = @[@1]; // Use IGListReloadDataUpdater to make sure updates happen synchronously IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListReloadDataUpdater new] viewController:nil]; adapter.collectionView = self.collectionView; adapter.dataSource = self.dataSource; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterPerformanceDelegate)]; adapter.performanceDelegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; // Sizing [[mockDelegate expect] listAdapterWillCallSize:adapter]; [[mockDelegate expect] listAdapter:adapter didCallSizeOnSectionController:OCMOCK_ANY atIndex:0]; // Dequeue [[mockDelegate expect] listAdapterWillCallDequeueCell:adapter]; [[mockDelegate expect] listAdapter:adapter didCallDequeueCell:OCMOCK_ANY onSectionController:OCMOCK_ANY atIndex:0]; // Display [[mockDelegate expect] listAdapterWillCallDisplayCell:adapter]; [[mockDelegate expect] listAdapter:adapter didCallDisplayCell:OCMOCK_ANY onSectionController:OCMOCK_ANY atIndex:0]; // End display [[mockDelegate expect] listAdapterWillCallEndDisplayCell:adapter]; [[mockDelegate expect] listAdapter:adapter didCallEndDisplayCell:OCMOCK_ANY onSectionController:OCMOCK_ANY atIndex:0]; // Commit changes [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; // Remove cell self.dataSource.objects = @[]; // Commit changes [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; [mockDelegate verify]; } - (void)test_whenScroll_thatPerformanceDelegateEventsSent { id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterPerformanceDelegate)]; self.adapter.performanceDelegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterWillCallScroll:self.adapter]; [[mockDelegate expect] listAdapter:self.adapter didCallScroll:self.collectionView]; [self.adapter scrollViewDidScroll:self.collectionView]; [mockDelegate verify]; } #pragma mark - Deleted Section Controllers - (void)test_whenSectionControllerRemoved_thatCellForIndexPathIsNil { self.dataSource.objects = @[@1]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:@1]; self.dataSource.objects = @[@2]; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNil([sectionController.collectionContext cellForItemAtIndex:0 sectionController:sectionController]); } - (void)test_whenSectionControllerRemoved_thatFullyVisibleCellsIsEmpty { self.dataSource.objects = @[@1]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:@1]; self.dataSource.objects = @[@2]; [self.adapter performUpdatesAnimated:NO completion:nil]; NSArray *cells = [sectionController.collectionContext fullyVisibleCellsForSectionController:sectionController]; XCTAssertEqual(cells.count, 0); } - (void)test_whenSectionControllerRemoved_thatVisibleCellsIsEmpty { self.dataSource.objects = @[@1]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:@1]; self.dataSource.objects = @[@2]; [self.adapter performUpdatesAnimated:NO completion:nil]; NSArray *cells = [sectionController.collectionContext visibleCellsForSectionController:sectionController]; XCTAssertEqual(cells.count, 0); } - (void)test_whenSectionControllerRemoved_thatVisibleIndexPathIsEmpty { self.dataSource.objects = @[@1]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:@1]; self.dataSource.objects = @[@2]; [self.adapter performUpdatesAnimated:NO completion:nil]; NSArray *cells = [sectionController.collectionContext visibleIndexPathsForSectionController:sectionController]; XCTAssertEqual(cells.count, 0); } - (void)test_whenSectionControllerRemoved_thatDoesNotCrashOnInvalidatingLayout { self.dataSource.objects = @[@1]; [self.adapter performUpdatesAnimated:NO completion:nil]; IGListSectionController *sectionController = [self.adapter sectionControllerForObject:@1]; self.dataSource.objects = @[@2]; [self.adapter performUpdatesAnimated:NO completion:nil]; [sectionController.collectionContext invalidateLayoutForSectionController:sectionController completion:nil]; } - (void)test_whenSettingSupplementaryView_thatViewForSupplementaryElementExists { self.dataSource.objects = @[@0]; [self.adapter reloadDataWithCompletion:nil]; IGTestSupplementarySource *supplementarySource = [IGTestSupplementarySource new]; supplementarySource.collectionContext = self.adapter; supplementarySource.supportedElementKinds = @[UICollectionElementKindSectionHeader]; IGListSectionController *controller = [self.adapter sectionControllerForObject:@0]; controller.supplementaryViewSource = supplementarySource; supplementarySource.sectionController = controller; [self.adapter performUpdatesAnimated:NO completion:nil]; XCTAssertNotNil([self.adapter viewForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:0 sectionController:controller]); XCTAssertNil([self.adapter viewForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:1 sectionController:controller]); } @end ================================================ FILE: Tests/IGListAdapterUpdaterTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterUpdaterHelpers.h" #import "IGListAdapterUpdaterInternal.h" #import "IGListMoveIndexInternal.h" #import "IGListTestUICollectionViewDataSource.h" #import "IGListTransitionData.h" #import "IGListUpdateTransactionBuilder.h" #import "IGTestObject.h" #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] #define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] #define genToBlock ^NSArray *{ return to; } @interface IGListAdapterUpdaterTests : XCTestCase @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListTestUICollectionViewDataSource *dataSource; @property (nonatomic, strong) IGListAdapterUpdater *updater; @property (nonatomic, strong) IGListTransitionDataApplyBlock applySectionDataBlock; @end @implementation IGListAdapterUpdaterTests - (IGListCollectionViewBlock)collectionViewBlock { return ^UICollectionView *{ return self.collectionView; }; } - (IGListTransitionDataBlock)dataBlockFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects { return ^IGListTransitionData *{ return [[IGListTransitionData alloc] initFromObjects:fromObjects toObjects:toObjects toSectionControllers:@[]]; }; } - (void)setUp { [super setUp]; self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:layout]; [self.window addSubview:self.collectionView]; self.dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:self.collectionView]; self.updater = [IGListAdapterUpdater new]; __weak __typeof__(self) weakSelf = self; self.applySectionDataBlock = ^(IGListTransitionData *data) { weakSelf.dataSource.sections = data.toObjects; }; } - (void)tearDown { [super tearDown]; self.collectionView = nil; self.dataSource = nil; self.updater = nil; } - (void)test_whenUpdatingtoObjects_thatUpdaterHasChanges { [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:@[] toObjects:@[@0]] applySectionDataBlock:self.applySectionDataBlock completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenUpdatingfromObjects_thatUpdaterHasChanges { [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[]] applySectionDataBlock:self.applySectionDataBlock completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenUpdatingtoObjects_withfromObjects_thatUpdaterHasChanges { [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:@[@0] toObjects:@[@1]] applySectionDataBlock:self.applySectionDataBlock completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenConvertingIndexPathsToSections_thatCorrectValuesAreReturned { NSArray *indexPaths = @[ [NSIndexPath indexPathForItem:0 inSection:0], [NSIndexPath indexPathForItem:0 inSection:1], [NSIndexPath indexPathForItem:0 inSection:2], [NSIndexPath indexPathForItem:0 inSection:3] ]; NSIndexSet *sections = IGListSectionIndexFromIndexPaths(indexPaths); XCTAssertEqual(sections.count, 4); XCTAssertEqual(sections.firstIndex, 0); XCTAssertEqual(sections.lastIndex, 3); } - (void)test_whenReloadingData_thatCollectionViewUpdates { self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[]]]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); self.dataSource.sections = @[]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 0); } - (void)test_whenReloadingDataWithNilDataSourceBefore_thatCollectionViewNotCrash { self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@2]]]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); self.collectionView.dataSource = nil; self.dataSource.sections = @[]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); // Setting collectionView's dataSource to nil would yield a single section by default. } - (void)test_whenInsertingSection_thatCollectionViewUpdates { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenDeletingSection_thatCollectionViewUpdates { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenInsertingSection_withItemChanges_thatCollectionViewUpdates { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[@0]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenInsertingSection_withDeletedSection_thatCollectionViewUpdates { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[@0, @1, @2]], [IGSectionObject sectionWithObjects:@[]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[@1, @1]], [IGSectionObject sectionWithObjects:@[@0]], [IGSectionObject sectionWithObjects:@[@0, @2, @3]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenReloadingSections_thatCollectionViewUpdates { self.dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); self.dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@0, @1, @2]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; [self.updater reloadCollectionView:self.collectionView sections:[NSIndexSet indexSetWithIndex:0]]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); } - (void)test_whenMovingSections_thatCollectionViewUpdates { self.dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1, @2]] ]; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); self.dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@0, @1, @2]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; [self.updater moveSectionInCollectionView:self.collectionView fromIndex:0 toIndex:1]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); } - (void)test_whenCollectionViewNeedsLayout_thatPerformBatchUpdateWorks { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; // the collection view has been setup with 1 section and now needs layout // calling performBatchUpdates: on a collection view needing layout will force layout // we need to ensure that our data source is not changed until the update block is executed [self.collectionView setNeedsLayout]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdatesAreReentrant_thatUpdatesExecuteSerially { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]], ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]], ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; __block NSInteger completionCounter = 0; XCTestExpectation *expectation1 = genExpectation; void (^preUpdateBlock)(void) = ^{ NSArray *anotherTo = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:to toObjects:anotherTo] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { completionCounter++; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual(completionCounter, 2); [expectation1 fulfill]; }]; }; XCTestExpectation *expectation2 = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:^(IGListTransitionData *data) { // executing this block within the updater is just before performBatchUpdates: are applied // should be able to queue another update here, similar to an update being queued between it beginning and executing // the performBatchUpdates: block preUpdateBlock(); self.dataSource.sections = data.toObjects; } completion:^(BOOL finished) { completionCounter++; XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual(completionCounter, 1); [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenQueuingItemUpdates_thatUpdaterHasChanges { [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{} completion:nil]; XCTAssertTrue([self.updater hasChanges]); } - (void)test_whenOnlyQueueingItemUpdates_thatUpdateBlockExecutes { XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ // expectation should be triggered. test failure is a timeout [expectation fulfill]; } completion:nil]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenQueueingItemUpdates_withBatchUpdate_thatItemUpdateBlockExecutes { __block BOOL itemUpdateBlockExecuted = NO; __block BOOL sectionUpdateBlockExecuted = NO; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:@[] toObjects:@[[IGSectionObject sectionWithObjects:@[@1]]]] applySectionDataBlock:^(IGListTransitionData * data) { self.dataSource.sections = data.toObjects; sectionUpdateBlockExecuted = YES; } completion:nil]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ itemUpdateBlockExecuted = YES; } completion:^(BOOL finished) { // test in the item completion block that the SECTION operations have been performed XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertTrue(itemUpdateBlockExecuted); XCTAssertTrue(sectionUpdateBlockExecuted); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenItemsMoveAndUpdate_thatCollectionViewWorks { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]], ]; // change the number of items in the section, which a move would be unable to handle and would throw // keep the same pointers so that the objects are equal [from[2] setObjects:@[@1]]; [from[0] setObjects:@[@1, @1]]; [from[1] setObjects:@[@1, @1, @1]]; NSArray *to = @[ from[2], from[0], from[1] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; // without moves as inserts, we would assert b/c the # of items in each section changes self.updater.sectionMovesAsDeletesInserts = YES; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 3); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenConvertingReloads_withoutChanges_thatOriginalIndexUsed { NSArray *from = @[]; NSArray *to = @[]; IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); NSMutableIndexSet *reloads = [result.updates mutableCopy]; [reloads addIndex:2]; NSMutableIndexSet *deletes = [result.deletes mutableCopy]; NSMutableIndexSet *inserts = [result.inserts mutableCopy]; IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 1); XCTAssertEqual(inserts.count, 1); XCTAssertTrue([deletes containsIndex:2]); XCTAssertTrue([inserts containsIndex:2]); } - (void)test_whenConvertingReloads_withChanges_thatIndexMoves { NSArray *from = @[@1, @2, @3]; NSArray *to = @[@3, @2, @1]; IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); NSMutableIndexSet *reloads = [result.updates mutableCopy]; [reloads addIndex:2]; NSMutableIndexSet *deletes = [result.deletes mutableCopy]; NSMutableIndexSet *inserts = [result.inserts mutableCopy]; IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 1); XCTAssertEqual(inserts.count, 1); XCTAssertTrue([deletes containsIndex:2]); XCTAssertTrue([inserts containsIndex:0]); } - (void)test_whenReloadingSection_whenSectionRemoved_thatConvertMethodCorrects { NSArray *from = @[@"a", @"b", @"c"]; NSArray *to = @[@"a", @"c"]; IGListIndexSetResult *result = IGListDiff(from, to, IGListDiffEquality); NSMutableIndexSet *reloads = [NSMutableIndexSet indexSetWithIndex:1]; NSMutableIndexSet *deletes = [NSMutableIndexSet new]; NSMutableIndexSet *inserts = [NSMutableIndexSet new]; IGListConvertReloadToDeleteInsert(reloads, deletes, inserts, result, from); XCTAssertEqual(reloads.count, 0); XCTAssertEqual(deletes.count, 0); XCTAssertEqual(inserts.count, 0); } - (void)test_whenReloadingData_withNilCollectionView_thatDelegateFinishesWithoutUpdates { id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; id compilerFriendlyNil = nil; [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; [self.updater update]; [mockDelegate verify]; } - (void)test_whenPerformingUpdates_withNilCollectionView_thatDelegateFinishesWithoutUpdates { id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; id compilerFriendlyNil = nil; [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:nil]; [self.updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return compilerFriendlyNil; } reloadUpdateBlock:^{} completion:nil]; [self.updater update]; [mockDelegate verify]; } - (void)test_whenCallingReloadData_withUICollectionViewFlowLayout_withEstimatedSize_thatSectionItemCountsCorrect { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; // setting the estimated size of a layout causes UICollectionView to requery layout attributes during reloadData // this becomes out of sync with the data source if the section/item count changes layout.estimatedItemSize = CGSizeMake(100, 10); UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:layout]; IGListTestUICollectionViewDataSource *dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:collectionView]; // 2 sections, 1 item in 1st, 4 items in 2nd dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]] ]; // assert the initial state of the collection view WITHOUT any layoutSubviews or anything XCTAssertEqual([collectionView numberOfSections], 2); XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([collectionView numberOfItemsInSection:1], 4); dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@1]], ]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; [updater update]; XCTAssertEqual([collectionView numberOfSections], 1); XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@1, @2, @3, @4]] ]; [updater reloadDataWithCollectionViewBlock:^UICollectionView *{ return collectionView; } reloadUpdateBlock:^{} completion:nil]; [updater update]; XCTAssertEqual([collectionView numberOfSections], 2); XCTAssertEqual([collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([collectionView numberOfItemsInSection:1], 4); } - (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isSetNO_diffHappens { [self.collectionView removeFromSuperview]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; NSArray *to = @[[IGSectionObject sectionWithObjects:@[]]]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:self.dataSource.sections toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { return result.inserts.firstIndex == 0 && result.moves.count == 0 && result.updates.count == 0 && result.deletes.count == 0; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadBatchedWithUpdate_thatCompletionBlockStillExecuted { IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; __block BOOL reloadDataCompletionExecuted = NO; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:^(BOOL finished) { reloadDataCompletionExecuted = YES; }]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ object.objects = @[@2, @1, @4, @5]; [self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:2 inSection:0], [NSIndexPath indexPathForItem:3 inSection:0], ]]; [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0], ]]; [self.updater moveItemInCollectionView:self.collectionView fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; } completion:^(BOOL finished) { XCTAssertTrue(reloadDataCompletionExecuted); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingItemUpdateInMiddleOfReload_thatCompletionBlockStillExecuted { IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; XCTestExpectation *expectation = genExpectation; // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. IGListReloadUpdateBlock reloadUpdateBlock = ^{ [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{} completion:^(BOOL finished) { [expectation fulfill]; }]; }; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:reloadUpdateBlock completion:nil]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingItemUpdateInMiddleOfUpdate_thatCompletionBlockStillExecuted { IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; XCTestExpectation *expectation = genExpectation; // Section-controllers can schedule item updates in -didUpdateToObject, so lets make sure the completion block works. void (^updateItemBlock)(void) = ^{ [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{} completion:^(BOOL finished) { [expectation fulfill]; }]; }; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] applySectionDataBlock:^(IGListTransitionData * _Nonnull data) { updateItemBlock(); } completion:nil]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenNotInViewHierarchy_thatUpdatesStillExecuteBlocks { [self.collectionView removeFromSuperview]; IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; __block BOOL objectTransitionBlockExecuted = NO; __block BOOL completionBlockExecuted = NO; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:self.dataSource.sections toObjects:self.dataSource.sections] applySectionDataBlock:^(IGListTransitionData *data) { objectTransitionBlockExecuted = YES; } completion:^(BOOL finished) { completionBlockExecuted = YES; }]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES itemUpdates:^{ object.objects = @[@2, @1, @4, @5]; [self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:2 inSection:0], [NSIndexPath indexPathForItem:3 inSection:0], ]]; [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0], ]]; [self.updater moveItemInCollectionView:self.collectionView fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; } completion:^(BOOL finished) { XCTAssertTrue(objectTransitionBlockExecuted); XCTAssertTrue(completionBlockExecuted); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenNotBatchUpdate_thatDelegateEventsSent { IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; [self.collectionView reloadData]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willDeleteIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; [[mockDelegate expect] listAdapterUpdater:self.updater willInsertIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; [[mockDelegate expect] listAdapterUpdater:self.updater willMoveFromIndexPath:OCMOCK_ANY toIndexPath:OCMOCK_ANY collectionView:self.collectionView]; [[mockDelegate expect] listAdapterUpdater:self.updater willReloadIndexPaths:OCMOCK_ANY collectionView:self.collectionView]; // This code is of no use, but it will let UICollectionView synchronize number of items, // so it will not crash in following updates. https://stackoverflow.com/a/46751421/2977647 [self.collectionView numberOfItemsInSection:0]; object.objects = @[@1, @2]; [self.updater deleteItemsFromCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0], ]]; object.objects = @[@1, @2, @4, @5]; [self.updater insertItemsIntoCollectionView:self.collectionView indexPaths:@[ [NSIndexPath indexPathForItem:2 inSection:0], [NSIndexPath indexPathForItem:3 inSection:0], ]]; object.objects = @[@2, @1, @4, @5]; [self.updater moveItemInCollectionView:self.collectionView fromIndexPath:[NSIndexPath indexPathForItem:2 inSection:0] toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; [self.updater reloadItemInCollectionView:self.collectionView fromIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] toIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; [mockDelegate verify]; } - (void)test_whenObjectIdentifiersCollide_withDifferentTypes_thatLookupReturnsNil { id testObject = [[IGTestObject alloc] initWithKey:@"foo" value:@"bar"]; id collision = @"foo"; XCTAssertEqual(collision, [testObject diffIdentifier]); IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; // mimic internal map setup in IGListAdapter NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions]; NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0]; [table setObject:@1 forKey:testObject]; XCTAssertNotNil([table objectForKey:testObject]); XCTAssertNil([table objectForKey:collision]); } - (void)test_whenReloadIsCalledWithSameItemCount_deleteInsertSectionHappen { IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] deleteSections:[NSIndexSet indexSetWithIndex:0] moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenPerformUpdates_dataSourceWasSetToNil_shouldNotCrash { NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater didFinishWithoutUpdatesWithCollectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; // Manually set the data source to be nil. self->_collectionView.dataSource = nil; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:^(IGListTransitionData *data) { } completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenPerformIndexPathUpdates_reloadingTheSameIndexPathMultipleTimes_shouldNotCrash { // Set up data NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; self.dataSource.sections = from; // Mock delegate to confirm update did work id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; // Expectation to wait for performUpdate to finish XCTestExpectation *expectation = genExpectation; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO itemUpdates:^{ [self.updater reloadItemInCollectionView:self.collectionView fromIndexPath:indexPath toIndexPath:indexPath]; [self.updater reloadItemInCollectionView:self.collectionView fromIndexPath:indexPath toIndexPath:indexPath]; } completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenPerformingUpdatesMultipleTimesInARow_thenUpdateWorks { NSArray *objects1 = @[ [IGSectionObject sectionWithObjects:@[@0]] ]; NSArray *objects2 = @[ [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; NSArray *objects3 = @[ [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]], [IGSectionObject sectionWithObjects:@[@0, @1]] ]; self.dataSource.sections = objects1; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:objects1 toObjects:objects2] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:objects2 toObjects:objects3] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished2) { XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 2); [expectation fulfill]; }]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenPerformingUpdate_thatCallsDiffingDelegate { self.updater.allowsBackgroundDiffing = YES; NSArray *from = @[ [IGSectionObject sectionWithObjects:@[] identifier:@"0"] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[] identifier:@"0"], [IGSectionObject sectionWithObjects:@[] identifier:@"1"] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willDiffFromObjects:from toObjects:to]; [[mockDelegate expect] listAdapterUpdater:self.updater didDiffWithResults:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { return [result.inserts isEqualToIndexSet:[NSIndexSet indexSetWithIndex:1]] && result.deletes.count == 0 && result.updates.count == 0 && result.moves.count == 0; }] onBackgroundThread:YES]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)_test_whenCollectionViewSectionCountIsIncorrect_thatDoesNotCrash { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]] ]; NSArray *to = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:YES sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; // Lets say we change the dataSource without the updater on accident. self.dataSource.sections = @[ [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]], [IGSectionObject sectionWithObjects:@[]] ]; // Lets force the collectionView to sync [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; XCTAssertEqual([self.collectionView numberOfSections], 3); // No we wait for the update, which should fallback to a reload. [self waitForExpectationsWithTimeout:30 handler:nil]; } # pragma mark - preferItemReloadsFroSectionReloads - (void)test_whenReloadIsCalledWithSameItemCount_andPreferItemReload_updateIndexPathsHappen { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet new] deleteSections:[NSIndexSet new] moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[[NSIndexPath indexPathForItem:0 inSection:0]] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; // Update the items NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadIsCalledWithDifferentItemCount_andPreferItemReload_deleteInsertSectionHappen { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] deleteSections:[NSIndexSet indexSetWithIndex:0] moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id"]]; // more items in the section NSArray *to = @[[IGSectionObject sectionWithObjects:@[@1, @2] identifier:@"id"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { return result.inserts.count == 0 && result.deletes.count == 0 && result.moves.count == 0 && result.updates.firstIndex == 0; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_andPreferItemReload_deleteInsertMoveHappens { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] deleteSections:[NSIndexSet indexSetWithIndex:1] moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"]]; // move section, and also update the item for "id2" NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"], [IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { if (result.inserts.count != 0 || result.deletes.count != 0) { return NO; } // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, and "id2" moved from section 1 -> 0 return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withDifferentSectionLength_andPreferItemReload_deleteInsertMoveHappens { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] deleteSections:[NSIndexSet indexSetWithIndex:1] moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1, @2, @3] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"]]; // move section, and also update the item for "id2" NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"], [IGSectionObject sectionWithObjects:@[@1, @2, @3] identifier:@"id1"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { if (result.inserts.count != 0 || result.deletes.count != 0) { return NO; } // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, and "id2" moved from section 1 -> 0 return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadIsCalledWithSectionMoveAndUpdate_withThreeSections_deleteInsertMoveHappens { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndex:0] deleteSections:[NSIndexSet indexSetWithIndex:1] moveSections:[NSSet setWithArray:@[[[IGListMoveIndex alloc] initWithFrom:0 to:1]]] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@2] identifier:@"id2"], [IGSectionObject sectionWithObjects:@[@3] identifier:@"id3"]]; // move section, and also update the items for "id2" NSArray *to = @[[IGSectionObject sectionWithObjects:@[@22, @23] identifier:@"id2"], [IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@3] identifier:@"id3"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { if (result.inserts.count != 0 || result.deletes.count != 0) { return NO; } // Make sure we note that index 1 is updated (id2 from @[@2] -> @[@22], "id1" moved from section 0 -> 1, "id2" moved from section 1 -> 0 return result.updates.firstIndex == 1 && result.moves.count == 2 && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:0 to:1]] && [result.moves containsObject:[[IGListMoveIndex alloc] initWithFrom:1 to:0]]; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_whenReloadIsCalledWithSectionInsertAndUpdate_andPreferItemReload_noItemReloads { self.updater.preferItemReloadsForSectionReloads = YES; IGListBatchUpdateData *expectedBatchUpdateData = [[IGListBatchUpdateData alloc] initWithInsertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)] deleteSections:[NSIndexSet indexSetWithIndex:0] moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id1"], [IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"]]; self.dataSource.sections = from; id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [mockDelegate setExpectationOrderMatters:YES]; [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { if (result.deletes.count != 0 || result.moves.count != 0) { return NO; } // Make sure we note that index 1 is updated (id1 from @[@1] -> @[@2]), and "id2" was inserted at index 1 return result.updates.firstIndex == 0 && result.inserts.firstIndex == 1; }] animated:NO]; [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:expectedBatchUpdateData collectionView:self.collectionView]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { [expectation fulfill]; }]; waitExpectation; [mockDelegate verify]; } - (void)test_withSingleItemSectionUpdates_thatPerformBatchUpdateWorks { self.updater.singleItemSectionUpdates = YES; IGSectionObject *first = [IGSectionObject sectionWithObjects:@[@1, @2, @3]]; IGSectionObject *second =[IGSectionObject sectionWithObjects:@[@4, @5, @6]]; NSArray *from = @[first, second]; NSArray *to = @[second, first]; self.dataSource.sections = from; [self.updater reloadDataWithCollectionViewBlock:[self collectionViewBlock] reloadUpdateBlock:^{} completion:nil]; [self.updater update]; XCTestExpectation *expectation = genExpectation; [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] animated:NO sectionDataBlock:[self dataBlockFromObjects:from toObjects:to] applySectionDataBlock:self.applySectionDataBlock completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 2); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } #pragma mark - Illegal state checking - (void)test_whenCollectionViewBlockIsNotCorrectlyApplied_thatTransactionsGetCancelled { // Test if a nil collection view block is accidentally supplied UICollectionView *(^listCollectionViewBlock)(void) = ^{ return self.collectionView; }; listCollectionViewBlock = nil; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater.transactionBuilder addReloadDataWithCollectionViewBlock:listCollectionViewBlock reloadBlock:^{} completion:nil]; [updater update]; XCTAssertNil(updater.lastTransactionBuilder); } - (void)test_whenReloadBlockIsNotCorrectlyApplied_thatTransactionsGetCancelled { // Test if a nil collection view block is accidentally supplied UICollectionView *(^listCollectionViewBlock)(void) = ^{ return self.collectionView; }; void (^reloadBlock)(void) = ^{}; reloadBlock = nil; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater.transactionBuilder addReloadDataWithCollectionViewBlock:listCollectionViewBlock reloadBlock:reloadBlock completion:nil]; [updater update]; XCTAssertNil(updater.lastTransactionBuilder); } - (void)test_whenDataSourceChangeBlockIsNotCorrectlyApplied_thatTransactionsGetCancelled { UICollectionView *(^listCollectionViewBlock)(void) = ^{ return self.collectionView; }; void (^dataSourceChangeBlock)(void) = ^{}; dataSourceChangeBlock = nil; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater.transactionBuilder addReloadDataWithCollectionViewBlock:listCollectionViewBlock reloadBlock:^{} completion:nil]; [updater.transactionBuilder addDataSourceChange:dataSourceChangeBlock]; [updater update]; XCTAssertNil(updater.lastTransactionBuilder); } - (void)test_whenAddingChangesFromTransactionBuilder_thatOperationExitsGracefully { IGListUpdateTransactionBuilder *builder = [IGListUpdateTransactionBuilder new]; builder = nil; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater.transactionBuilder addChangesFromBuilder:builder]; } - (void)test_whenCallingWillCrash_thatDelegateIsCalled { id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; self.updater.delegate = mockDelegate; [[mockDelegate expect] listAdapterUpdater:self.updater willCrashWithCollectionView:self.collectionView sectionControllerClass:[NSObject class]]; [self.updater willCrashWithCollectionView:self.collectionView sectionControllerClass:[NSObject class]]; [mockDelegate verify]; } - (void)test_whenSettingAdaptiveCoalescingConfig_thatGetterReturnsConfig { IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.1, .intervalIncrement = 0.05, .maxInterval = 0.5, .useMaxIntervalWhenViewNotVisible = YES }; self.updater.adaptiveCoalescingExperimentConfig = config; IGListAdaptiveCoalescingExperimentConfig retrievedConfig = self.updater.adaptiveCoalescingExperimentConfig; XCTAssertTrue(retrievedConfig.enabled); XCTAssertEqual(retrievedConfig.minInterval, 0.1); XCTAssertEqual(retrievedConfig.intervalIncrement, 0.05); XCTAssertEqual(retrievedConfig.maxInterval, 0.5); XCTAssertTrue(retrievedConfig.useMaxIntervalWhenViewNotVisible); } - (void)test_whenBuildingTransactionWithoutCollectionViewBlock_thatReturnsNil { IGListUpdateTransactionBuilder *builder = [IGListUpdateTransactionBuilder new]; IGListUpdateTransactationConfig config = { .sectionMovesAsDeletesInserts = NO, .singleItemSectionUpdates = NO, .allowsReloadingOnTooManyUpdates = YES, .allowsBackgroundDiffing = NO, .experiments = IGListExperimentNone }; id transaction = [builder buildWithConfig:config delegate:nil updater:self.updater]; XCTAssertNil(transaction); } @end ================================================ FILE: Tests/IGListBatchUpdateDataTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListMoveIndexPathInternal.h" // IGListMoveIndexInternal.h @interface IGListMoveIndex (Private) - (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to; @end @interface IGListBatchUpdateDataTests : XCTestCase @end @implementation IGListBatchUpdateDataTests static NSIndexSet *indexSet(NSArray *arr) { NSMutableIndexSet *set = [NSMutableIndexSet new]; for (NSNumber *n in arr) { [set addIndex:[n integerValue]]; } return set; } static NSIndexPath *newPath(NSInteger section, NSInteger item) { return [NSIndexPath indexPathForItem:item inSection:section]; } static IGListMoveIndexPath *newMovePath(NSInteger fromSection, NSInteger fromItem, NSInteger toSection, NSInteger toItem) { return [[IGListMoveIndexPath alloc] initWithFrom:newPath(fromSection, fromItem) to:newPath(toSection, toItem)]; } static IGListMoveIndex *newMove(NSInteger from, NSInteger to) { return [[IGListMoveIndex alloc] initWithFrom:from to:to]; } - (void)test_whenEmptyUpdates_thatResultExists { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertNotNil(result); } - (void)test_whenUpdatesAreClean_thatResultMatches { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[@0, @1]) deleteSections:indexSet(@[@5]) moveSections:[NSSet setWithArray:@[newMove(3, 4)]] insertIndexPaths:@[newPath(0, 0)] deleteIndexPaths:@[newPath(1, 0)] updateIndexPaths:@[] moveIndexPaths:@[newMovePath(6, 0, 6, 1)]]; XCTAssertEqualObjects(result.insertSections, indexSet(@[@0, @1])); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@5])); XCTAssertEqualObjects(result.moveSections, [NSSet setWithArray:@[newMove(3, 4)]]); XCTAssertEqualObjects(result.insertIndexPaths, @[newPath(0, 0)]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(1, 0)]); XCTAssertEqual(result.moveIndexPaths.count, 1); XCTAssertEqualObjects(result.moveIndexPaths.firstObject, [[IGListMoveIndexPath alloc] initWithFrom:newPath(6, 0) to:newPath(6, 1)]); } - (void)test_whenMovingSections_withItemDeletes_thatResultConvertsConflicts_toDeletesAndInserts { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet setWithArray:@[newMove(2, 4)]] insertIndexPaths:@[] deleteIndexPaths:@[newPath(2, 0), newPath(3, 4)] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqualObjects(result.insertSections, indexSet(@[@4])); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@2])); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(3, 4)]); XCTAssertEqual(result.moveSections.count, 0); } - (void)test_whenMovingSections_withItemInserts_thatResultConvertsConflicts_toDeletesAndInserts { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet setWithArray:@[newMove(2, 4)]] insertIndexPaths:@[newPath(4, 0), newPath(3, 4)] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqualObjects(result.insertSections, indexSet(@[@4])); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@2])); XCTAssertEqualObjects(result.insertIndexPaths, @[newPath(3, 4)]); XCTAssertEqual(result.moveSections.count, 0); } - (void)test_whenMovingIndexPaths_withSectionDeleted_thatResultDropsTheMove { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[@0]) moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[newMovePath(0, 0, 0, 1)]]; XCTAssertEqual(result.moveIndexPaths.count, 0); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@0])); } - (void)test_whenMovingIndexPaths_withSectionMoved_thatResultConvertsToDeletesAndInserts { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet setWithArray:@[newMove(0, 1)]] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[newMovePath(0, 0, 0, 1)]]; XCTAssertEqual(result.moveIndexPaths.count, 0); XCTAssertEqual(result.moveSections.count, 0); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@0])); XCTAssertEqualObjects(result.insertSections, indexSet(@[@1])); } - (void)test_whenMovingSections_withMoveFromConflictWithDelete_thatResultDropsTheMove { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[@2]) moveSections:[NSSet setWithArray:@[newMove(2, 6), newMove(0, 2)]] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqual(result.deleteSections.count, 1); XCTAssertEqual(result.moveSections.count, 1); XCTAssertEqual(result.insertSections.count, 0); XCTAssertEqualObjects(result.deleteSections, indexSet(@[@2])); XCTAssertEqualObjects(result.moveSections.anyObject, newMove(0, 2)); } - (void)test_whenDeletingSameIndexPathMultipleTimes_thatResultDropsTheDuplicates { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenNoInsertingAndDeletingTwice_thatResultDropsTheDuplicatesWithKeepsSameNetItemCount { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqualObjects(result.insertIndexPaths, @[]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenInsertingOnceAndDeletingTwice_thatResultDropsTheDuplicatesWithKeepsSameNetItemCount { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[newPath(2, 0)] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; // Should remove one insert XCTAssertEqualObjects(result.insertIndexPaths, @[]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenInsertingTwiceAndDeletingTwice_thatResultDropsTheDuplicatesWithKeepsSameNetItemCount { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[newPath(2, 0), newPath(2, 0)] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; // Should remove one insert XCTAssertEqualObjects(result.insertIndexPaths, @[newPath(2, 0)]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenInsertingThriceAndDeletingTwice_thatResultDropsTheDuplicatesWithKeepsSameNetItemCount { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[newPath(2, 0), newPath(2, 0), newPath(2, 0)] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; // Should remove one insert NSArray *const expectedInsertIndexPaths = @[newPath(2, 0), newPath(2, 0)]; XCTAssertEqualObjects(result.insertIndexPaths, expectedInsertIndexPaths); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenInsertingThriceAndDeletingThrice_thatResultDropsTheDuplicatesWithKeepsSameNetItemCount { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[newPath(2, 0), newPath(2, 0), newPath(2, 0)] deleteIndexPaths:@[newPath(2, 0), newPath(2, 0), newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; // Should remove 2 inserts XCTAssertEqualObjects(result.insertIndexPaths, @[newPath(2, 0)]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenInsertingOnceAndDeletingOnce_thatNoThingChanges { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[newPath(2, 0)] deleteIndexPaths:@[newPath(2, 0)] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertEqualObjects(result.insertIndexPaths, @[newPath(2, 0)]); XCTAssertEqualObjects(result.deleteIndexPaths, @[newPath(2, 0)]); } - (void)test_whenUpdatesAreClean_thatObjectIsEqualToItself { IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[@0, @1]) deleteSections:indexSet(@[@5]) moveSections:[NSSet setWithArray:@[newMove(3, 4)]] insertIndexPaths:@[newPath(0, 0)] deleteIndexPaths:@[newPath(1, 0)] updateIndexPaths:@[] moveIndexPaths:@[newMovePath(6, 0, 6, 1)]]; XCTAssertTrue([result isEqual:result]); } - (void)test_whenEmptyUpdates_thatResultDoesNotEqualOtherClasses { IGListBatchUpdateData *emptyResult = [[IGListBatchUpdateData alloc] initWithInsertSections:indexSet(@[]) deleteSections:indexSet(@[]) moveSections:[NSSet new] insertIndexPaths:@[] deleteIndexPaths:@[] updateIndexPaths:@[] moveIndexPaths:@[]]; XCTAssertFalse([emptyResult isEqual:[NSObject new]]); } @end ================================================ FILE: Tests/IGListBindingSectionControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterInternal.h" #import "IGListTestCase.h" #import "IGTestBindingWithoutDeselectionDelegate.h" #import "IGTestCell.h" #import "IGTestDiffingDataSource.h" #import "IGTestDiffingObject.h" #import "IGTestDiffingSectionController.h" #import "IGTestInvalidateLayoutDataSource.h" #import "IGTestInvalidateLayoutObject.h" #import "IGTestNumberBindableCell.h" #import "IGTestObject.h" #import "IGTestStringBindableCell.h" @interface IGListBindingSectionController (Tests) - (void)setState:(NSInteger)state; @end @interface IGListBindingSectionControllerTests : IGListTestCase @end @implementation IGListBindingSectionControllerTests - (void)setUp { self.dataSource = [IGTestDiffingDataSource new]; // give us more room to show cells self.frame = CGRectMake(0, 0, 100, 1000); [super setUp]; } - (id)cellAtSection:(NSInteger)section item:(NSInteger)item { return [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:item inSection:section]]; } - (void)test_whenInitialLoad_withEmptyViewModels_thatCollectionViewEmpty { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[]] ]]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 0); } - (void)test_whenInitialLoad_withMultipleViewModels_thatCellsMappedAndConfigured { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], [[IGTestDiffingObject alloc] initWithKey:@2 objects:@[@"foo", @"bar", @42]], [[IGTestDiffingObject alloc] initWithKey:@3 objects:@[]], ]]; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 0); IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1]; IGTestStringBindableCell *cell10 = [self cellAtSection:1 item:0]; IGTestStringBindableCell *cell11 = [self cellAtSection:1 item:1]; IGTestNumberBindableCell *cell12 = [self cellAtSection:1 item:2]; XCTAssertEqualObjects(cell00.textField.text, @"7"); XCTAssertEqualObjects(cell01.label.text, @"seven"); XCTAssertEqualObjects(cell10.label.text, @"foo"); XCTAssertEqualObjects(cell11.label.text, @"bar"); XCTAssertEqualObjects(cell12.textField.text, @"42"); } - (void)test_withDuplicateDiffIdentifiers_thatDuplicatesAreRemoved { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @7]], ]]; XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0]; XCTAssertEqualObjects(cell00.textField.text, @"7"); } - (void)test_whenUpdating_withAddedModels_thatCellsCorrectAndConfigured { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; self.dataSource.objects = @[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven", @8, @"eight"]], ]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { XCTAssertEqual([self.collectionView numberOfSections], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 4); IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1]; IGTestNumberBindableCell *cell02 = [self cellAtSection:0 item:2]; IGTestStringBindableCell *cell03 = [self cellAtSection:0 item:3]; XCTAssertEqualObjects(cell00.textField.text, @"7"); XCTAssertEqualObjects(cell01.label.text, @"seven"); XCTAssertEqualObjects(cell02.textField.text, @"8"); XCTAssertEqualObjects(cell03.label.text, @"eight"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdating_withNotUniqueModels_thatCellsCorrectAndConfigured { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter reloadObjects:@[[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@"four", @4, @"seven", @7, @10]]]]; IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1]; XCTAssertEqualObjects(cell00.textField.text, @"7"); XCTAssertEqualObjects(cell01.label.text, @"seven"); XCTAssertNil([self cellAtSection:0 item:2]); XCTAssertNil([self cellAtSection:0 item:3]); // "fake" batch updates to make sure that calling reload triggers a diffed batch update XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performBatchAnimated:YES updates:^(id batchContext){} completion:^(BOOL finished) { IGTestStringBindableCell *batchedCell00 = [self cellAtSection:0 item:0]; IGTestNumberBindableCell *batchedCell01 = [self cellAtSection:0 item:1]; IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2]; IGTestNumberBindableCell *batchedCell03 = [self cellAtSection:0 item:3]; IGTestNumberBindableCell *batchedCell04 = [self cellAtSection:0 item:4]; XCTAssertEqualObjects(batchedCell00.label.text, @"four"); XCTAssertEqualObjects(batchedCell01.textField.text, @"4"); XCTAssertEqualObjects(batchedCell02.label.text, @"seven"); XCTAssertEqualObjects(batchedCell03.textField.text, @"7"); XCTAssertEqualObjects(batchedCell04.textField.text, @"10"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSelectingCell_thatCorrectViewModelSelected { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.selectedViewModel, @"seven"); } - (void)test_whenDeselectingCell_thatCorrectViewModelSelected { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.deselectedViewModel, @"seven"); } - (void)test_whenHighlightingCell_thatCorrectViewModelHighlighted { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter collectionView:self.collectionView didHighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.highlightedViewModel, @"seven"); } - (void)test_whenUnhighlightingCell_thatCorrectViewModelUnhighlighted { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter collectionView:self.collectionView didUnhighlightItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.unhighlightedViewModel, @"seven"); } #if !TARGET_OS_TV - (void)test_whenContextMenuAskedCell_thatCorrectViewModelRetrieved API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter collectionView:self.collectionView contextMenuConfigurationForItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0] point:CGPointZero]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertEqualObjects(section.contextMenuViewModel, @"seven"); } #endif - (void)test_whenDeselectingCell_withoutImplementation_thatNoOps { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; IGTestBindingWithoutDeselectionDelegate *delegate = [IGTestBindingWithoutDeselectionDelegate new]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; section.selectionDelegate = delegate; [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; XCTAssertFalse(delegate.selected); [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; XCTAssertTrue(delegate.selected); } - (void)test_whenAdapterReloadsObjects_thatSectionUpdated { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter reloadObjects:@[[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@"four", @4, @"seven", @7]]]]; IGTestNumberBindableCell *cell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1]; XCTAssertEqualObjects(cell00.textField.text, @"7"); XCTAssertEqualObjects(cell01.label.text, @"seven"); XCTAssertNil([self cellAtSection:0 item:2]); XCTAssertNil([self cellAtSection:0 item:3]); // "fake" batch updates to make sure that calling reload triggers a diffed batch update XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performBatchAnimated:YES updates:^(id batchContext){} completion:^(BOOL finished) { IGTestStringBindableCell *batchedCell00 = [self cellAtSection:0 item:0]; IGTestNumberBindableCell *batchedCell01 = [self cellAtSection:0 item:1]; IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2]; IGTestNumberBindableCell *batchedCell03 = [self cellAtSection:0 item:3]; XCTAssertEqualObjects(batchedCell00.label.text, @"four"); XCTAssertEqualObjects(batchedCell01.textField.text, @"4"); XCTAssertEqualObjects(batchedCell02.label.text, @"seven"); XCTAssertEqualObjects(batchedCell03.textField.text, @"7"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdating_withViewModelMovesAndReloads_thatCellUpdatedAndInstanceSame { NSArray *initObjects = @[ @"foo", @"bar", [[IGTestObject alloc] initWithKey:@42 value:@"baz"] ]; [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:initObjects] ]]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); IGTestStringBindableCell *cell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *cell01 = [self cellAtSection:0 item:1]; IGTestCell *cell02 = [self cellAtSection:0 item:2]; XCTAssertEqualObjects(cell00.label.text, @"foo"); XCTAssertEqualObjects(cell01.label.text, @"bar"); XCTAssertEqualObjects(cell02.label.text, @"baz"); NSArray *newObjects = @[ [[IGTestObject alloc] initWithKey:@42 value:@"bang"], // moved to section 0 and value changed @"foo", @"bar", ]; self.dataSource.objects = @[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:newObjects] ]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestCell *batchedCell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *batchedCell01 = [self cellAtSection:0 item:1]; IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2]; XCTAssertEqualObjects(batchedCell00.label.text, @"bang"); XCTAssertEqualObjects(batchedCell01.label.text, @"foo"); XCTAssertEqualObjects(batchedCell02.label.text, @"bar"); XCTAssertEqual(cell00, batchedCell01); XCTAssertEqual(cell01, batchedCell02); XCTAssertEqual(cell02, batchedCell00); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdating_withViewModelDeletionsMovesAndReloads_thatCellUpdatedAndInstanceSame { NSArray *startingSection0 = @[ @"0", @"1", [[IGTestObject alloc] initWithKey:@0 value:@"2"] ]; NSArray *startingSection1 = @[ @"a", @"b", [[IGTestObject alloc] initWithKey:@1 value:@"c"], [[IGTestObject alloc] initWithKey:@2 value:@"d"], ]; [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@0 objects:startingSection0], [[IGTestDiffingObject alloc] initWithKey:@1 objects:startingSection1] ]]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 4); IGTestStringBindableCell *cell10 = [self cellAtSection:1 item:0]; IGTestStringBindableCell *cell11 = [self cellAtSection:1 item:1]; IGTestCell *cell12 = [self cellAtSection:1 item:2]; IGTestCell *cell13 = [self cellAtSection:1 item:3]; XCTAssertEqualObjects(cell10.label.text, @"a"); XCTAssertEqualObjects(cell11.label.text, @"b"); XCTAssertEqualObjects(cell12.label.text, @"c"); XCTAssertEqualObjects(cell13.label.text, @"d"); // Moves: // - Delete section 0. // - Modify section 1 in several ways: NSArray *newSection1 = @[ [[IGTestObject alloc] initWithKey:@1 value:@"e"], // Index: 2 -> 0, Value: "c" -> "e" @"b", // No change. @"a", // Index: 0 -> 2 [[IGTestObject alloc] initWithKey:@2 value:@"f"], // Value: "d" -> "f" ]; self.dataSource.objects = @[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:newSection1] ]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestCell *batchedCell00 = [self cellAtSection:0 item:0]; IGTestStringBindableCell *batchedCell01 = [self cellAtSection:0 item:1]; IGTestStringBindableCell *batchedCell02 = [self cellAtSection:0 item:2]; IGTestCell *batchedCell03 = [self cellAtSection:0 item:3]; XCTAssertEqualObjects(batchedCell00.label.text, @"e"); XCTAssertEqualObjects(batchedCell01.label.text, @"b"); XCTAssertEqualObjects(batchedCell02.label.text, @"a"); XCTAssertEqualObjects(batchedCell03.label.text, @"f"); XCTAssertEqual(cell10, batchedCell02); XCTAssertEqual(cell11, batchedCell01); XCTAssertEqual(cell12, batchedCell00); XCTAssertEqual(cell13, batchedCell03); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdatingManually_with2Updates_thatBothCompletionBlocksCalled { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects[0]]; XCTestExpectation *expectation1 = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [section updateAnimated:YES completion:^(BOOL updated) { XCTAssertTrue(updated); [expectation1 fulfill]; }]; XCTestExpectation *expectation2 = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [section updateAnimated:YES completion:^(BOOL updated) { // queued second, shouldn't execute update block XCTAssertFalse(updated); [expectation2 fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenUpdating_withMutableArrayObject_thatViewModelsDontMutate { NSArray *objects = @[ @"foo", @"bar" ]; NSMutableArray *initObjects = [NSMutableArray arrayWithArray:objects]; [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:initObjects] ]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; XCTAssertNotEqual(initObjects, section.viewModels); XCTAssertEqualObjects(initObjects, section.viewModels); [initObjects removeAllObjects]; XCTAssertNotEqualObjects(initObjects, section.viewModels); } - (void)test_whenUpdatingManully_withInvalidateLayoutForUpdates_thatCellSizeUpdatedToLatestSize_usingIGListCollectionViewLayout { self.dataSource = [[IGTestInvalidateLayoutDataSource alloc] init]; self.adapter.dataSource = self.dataSource; NSArray *startingSection0 = @[ genInvalidateLayoutObject(@0, CGSizeMake(50, 30)), genInvalidateLayoutObject(@1, CGSizeMake(50, 40)), ]; NSArray *startingSection1 = @[ genInvalidateLayoutObject(@0, CGSizeMake(50, 50)), genInvalidateLayoutObject(@1, CGSizeMake(50, 60)) ]; IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:NO]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:layout]; self.adapter.experiments |= IGListExperimentInvalidateLayoutForUpdates; [self setupWithObjects:@[ [[IGTestInvalidateLayoutObject alloc] initWithKey:@0 objects:startingSection0], [[IGTestInvalidateLayoutObject alloc] initWithKey:@1 objects:startingSection1] ]]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 2); IGTestCell *cell00 = [self cellAtSection:0 item:0]; IGTestCell *cell01 = [self cellAtSection:0 item:1]; IGTestCell *cell10 = [self cellAtSection:1 item:0]; IGTestCell *cell11 = [self cellAtSection:1 item:1]; IGAssertEqualSize(cell00.frame.size, 50, 30); IGAssertEqualSize(cell01.frame.size, 50, 40); IGAssertEqualSize(cell10.frame.size, 50, 50); IGAssertEqualSize(cell11.frame.size, 50, 60); NSArray *newSection0 = @[ genInvalidateLayoutObject(@0, CGSizeMake(45, 30)), // Width: 50 -> 45 genInvalidateLayoutObject(@1, CGSizeMake(50, 55)), // Height: 40 -> 55 ]; NSArray *newSection1 = @[ genInvalidateLayoutObject(@0, CGSizeMake(50, 50)), // No change genInvalidateLayoutObject(@1, CGSizeMake(20, 30)), // Size: (50, 60) -> (20, 30) ]; self.dataSource.objects = @[ [[IGTestInvalidateLayoutObject alloc] initWithKey:@0 objects:newSection0], [[IGTestInvalidateLayoutObject alloc] initWithKey:@1 objects:newSection1] ]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { [self.collectionView layoutIfNeeded]; IGTestCell *updatedCell00 = [self cellAtSection:0 item:0]; IGTestCell *updatedCell01 = [self cellAtSection:0 item:1]; IGTestCell *nochangedCell10 = [self cellAtSection:1 item:0]; IGTestCell *updatedCell11 = [self cellAtSection:1 item:1]; IGAssertEqualSize(updatedCell00.frame.size, 45, 30); IGAssertEqualSize(updatedCell01.frame.size, 50, 55); IGAssertEqualSize(nochangedCell10.frame.size, 50, 50); IGAssertEqualSize(updatedCell11.frame.size, 20, 30); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_viewModelsUpdate_afterCellHasBeenMoved { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven", @20]], ]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; [section moveObjectFromIndex:0 toIndex:2]; XCTAssertEqual([section.viewModels firstObject], @"seven"); XCTAssertEqual([section.viewModels lastObject], @7); [section moveObjectFromIndex:2 toIndex:1]; XCTAssertEqual([section.viewModels objectAtIndex: 1], @7); XCTAssertEqual([section.viewModels lastObject], @20); } #pragma mark - Illegal state validation - (void)test_whenAdapterReloadsObjects_andStateIsInconsistent_thatExecutionCompletes { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; [self.adapter reloadObjects:@[[[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@"four", @4, @"seven", @7]]]]; // Queue up the batch operation which will be called on the next main run loop XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.adapter performBatchAnimated:YES updates:^(id batchContext){} completion:^(BOOL finished) { [expectation fulfill]; }]; // Before the next run loop triggers, set the state back to idle IGListBindingSectionController *controller = (IGListBindingSectionController *)[self.adapter sectionControllerForSection:0]; [controller setState:0]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSizeForItemAtIndex_withInvalidIndex_thatReturnsCGSizeZero { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; // Index out of bounds (too large) CGSize sizeOutOfBounds = [section sizeForItemAtIndex:100]; XCTAssertTrue(CGSizeEqualToSize(sizeOutOfBounds, CGSizeZero)); // Negative index CGSize sizeNegative = [section sizeForItemAtIndex:-1]; XCTAssertTrue(CGSizeEqualToSize(sizeNegative, CGSizeZero)); } #if !TARGET_OS_TV - (void)test_whenContextMenuConfiguration_withNoSelectionDelegate_thatReturnsNil { [self setupWithObjects:@[ [[IGTestDiffingObject alloc] initWithKey:@1 objects:@[@7, @"seven"]], ]]; IGTestDiffingSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; section.selectionDelegate = nil; if (@available(iOS 13.0, *)) { UIContextMenuConfiguration *config = [section contextMenuConfigurationForItemAtIndex:0 point:CGPointZero]; XCTAssertNil(config); } } #endif @end ================================================ FILE: Tests/IGListBindingSingleSectionControllerTests.m ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGTestCell.h" #import "IGListTestCase.h" #import "IGListAdapterInternal.h" #import "IGTestCell.h" #import "IGListBindingSingleSectionController.h" #import "IGTestBindingSingleItemDataSource.h" @interface IGListBindingSingleSectionControllerTests : IGListTestCase @end @implementation IGListBindingSingleSectionControllerTests - (void)setUp { self.dataSource = [IGTestBindingSingleItemDataSource new]; self.frame = CGRectMake(0, 0, 100, 1000); [super setUp]; } - (void)test_withDefaultClass_thatCellOverrideMethodsFallThroughCorrectly { IGListBindingSingleSectionController *controller = [IGListBindingSingleSectionController new]; id viewModel = genTestObject(@1, @"Foo"); UICollectionViewCell *cell = [UICollectionViewCell new]; XCTAssertNoThrow([controller didSelectItemWithCell:cell]); XCTAssertNoThrow([controller didDeselectItemWithCell:cell]); XCTAssertNoThrow([controller didHighlightItemWithCell:cell]); XCTAssertNoThrow([controller didUnhighlightItemWithCell:cell]); @try { [controller cellClass]; } @catch (NSException *e) {} @try { [controller configureCell:cell withViewModel:viewModel]; } @catch (NSException *e) {} @try { [controller sizeForViewModel:viewModel]; } @catch (NSException *e) {} } - (void)test_whenSetupWithObjects_collectionViewHasSections { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1); } - (void)test_whenSetupWithObjects_sizeIsCalled { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; // Check the size is set in `IGTestBindingSingleSectionController` XCTAssertEqual(cell1.frame.size.height, 44); XCTAssertEqual(cell2.frame.size.height, 44); XCTAssertEqual(cell3.frame.size.height, 44); XCTAssertEqual(cell1.frame.size.width, 100); XCTAssertEqual(cell2.frame.size.width, 100); XCTAssertEqual(cell3.frame.size.width, 100); } - (void)test_whenSetupWithObjects_scrollingIsPerformedCorrectly { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; // Check that all 3 cells are valid XCTAssertNotNil(cell1); XCTAssertNotNil(cell2); XCTAssertNotNil(cell3); // Scroll the collection view enough for the first cell to be out of bounds self.collectionView.contentOffset = (CGPoint){0, 45}; [self.collectionView layoutIfNeeded]; cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; // Check that all cells except the first are valid XCTAssertNil(cell1); XCTAssertNotNil(cell2); XCTAssertNotNil(cell3); } - (void)test_whenSetupWithObjects_cellsAreConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; // Check the cell is configured in `IGTestBindingSingleSectionController` XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); XCTAssertEqualObjects(cell3.label.text, @"Baz"); } - (void)test_whenSetupWithObjects_cellClassIsExpected { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue([cell isKindOfClass:[IGTestCell class]]); } - (void)test_whenSetupWithObjects_andReloadingObjects_diffingBehaviorIsExpected { // Set up with an initial data set [self setupWithObjects:@[genTestObject(@1, @"Foo")]]; IGTestCell *cell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqualObjects(cell.label.text, @"Foo"); // Create a dispatch group so we can force the async reloads to happen serially XCTestExpectation *expectation1 = genExpectation; // Regenerate a new copy of the original data set self.dataSource.objects = @[genTestObject(@1, @"Foo")]; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { [expectation1 fulfill]; }]; [self waitForExpectations:@[expectation1] timeout:30]; IGTestCell *reloadedCell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqualObjects(reloadedCell.label.text, @"Foo"); XCTestExpectation *expectation2 = genExpectation; // Set a data set with new values self.dataSource.objects = @[genTestObject(@1, @"Bar")]; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { [expectation2 fulfill]; }]; [self waitForExpectations:@[expectation2] timeout:30]; IGTestCell *modifiedCell = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertEqualObjects(modifiedCell.label.text, @"Bar"); } - (void)test_whenDidSelectIsCalled_subclassIsCalled { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), ]]; IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; [controller didSelectItemAtIndex:0]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; // Check the cell is being displayed IGListBindingSingleSectionController *singleSectionController = (IGListBindingSingleSectionController *)controller; XCTAssertTrue([singleSectionController isDisplayingCell]); // Check the cell label is updated in `IGTestBindingSingleSectionController` XCTAssertEqualObjects(cell1.label.text, @"did-select"); } - (void)test_whenDidDeselectIsCalled_subclassIsCalled { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), ]]; IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; [controller didDeselectItemAtIndex:0]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; // Check the cell label is updated in `IGTestBindingSingleSectionController` XCTAssertEqualObjects(cell1.label.text, @"did-deselect"); } - (void)test_whenDidHighlightIsCalled_subclassIsCalled { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), ]]; IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; [controller didHighlightItemAtIndex:0]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; // Check the cell label is updated in `IGTestBindingSingleSectionController` XCTAssertEqualObjects(cell1.label.text, @"did-highlight"); } - (void)test_whenDidUnhighlightIsCalled_subclassIsCalled { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), ]]; IGListSectionController *controller = [self.adapter sectionControllerForSection:0]; [controller didUnhighlightItemAtIndex:0]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; // Check the cell label is updated in `IGTestBindingSingleSectionController` XCTAssertEqualObjects(cell1.label.text, @"did-unhighlight"); } @end ================================================ FILE: Tests/IGListCollectionScrollingTraitsTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import @interface IGListCollectionScrollingTraitsTests : XCTestCase @property (nonatomic, strong) id collectionContext; @property (nonatomic, strong) id mockCollectionView; @end @implementation IGListCollectionScrollingTraitsTests - (void)setUp { [super setUp]; self.mockCollectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter.collectionView = self.mockCollectionView; self.collectionContext = (id)adapter; } - (void)test_whenTracking_thatIsTrackingReturnsTrue { [[[self.mockCollectionView stub] andReturnValue:@YES] isTracking]; XCTAssertTrue(self.collectionContext.scrollingTraits.isTracking); } - (void)test_whenNotTracking_thatIsTrackingReturnsFalse { [[[self.mockCollectionView stub] andReturnValue:@NO] isTracking]; XCTAssertFalse(self.collectionContext.scrollingTraits.isTracking); } - (void)test_whenDragging_thatIsDraggingReturnsTrue { [[[self.mockCollectionView stub] andReturnValue:@YES] isDragging]; XCTAssertTrue(self.collectionContext.scrollingTraits.isDragging); } - (void)test_whenDragging_thatIsDraggingReturnsFalse { [[[self.mockCollectionView stub] andReturnValue:@NO] isDragging]; XCTAssertFalse(self.collectionContext.scrollingTraits.isDragging); } - (void)test_whenDecelerating_thatIsDeceleratingReturnsTrue { [[[self.mockCollectionView stub] andReturnValue:@YES] isDecelerating]; XCTAssertTrue(self.collectionContext.scrollingTraits.isDecelerating); } - (void)test_whenDecelerating_thatIsDeceleratingReturnsFalse { [[[self.mockCollectionView stub] andReturnValue:@NO] isDecelerating]; XCTAssertFalse(self.collectionContext.scrollingTraits.isDecelerating); } @end ================================================ FILE: Tests/IGListCollectionViewLayoutTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGLayoutTestDataSource.h" #import "IGLayoutTestItem.h" #import "IGLayoutTestSection.h" #import "IGListAdapter.h" #import "IGListAdapterProxy.h" #import "IGListAdapterUpdater.h" #import "IGListCollectionViewLayoutInternal.h" #import "IGListTestHelpers.h" @interface IGListCollectionViewLayout (Tests) - (NSString *)_classNameForDelegate:(id)delegate sectionIndex:(NSInteger)section; @end @interface IGListCollectionViewLayoutTests : XCTestCase @property (nonatomic, strong) IGListCollectionViewLayout *layout; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGLayoutTestDataSource *dataSource; @end static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}}; @implementation IGListCollectionViewLayoutTests - (UICollectionViewCell *)cellForSection:(NSInteger)section item:(NSInteger)item { return [self.collectionView cellForItemAtIndexPath:genIndexPath(section, item)]; } - (UICollectionReusableView *)headerForSection:(NSInteger)section { return [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionHeader atIndexPath:genIndexPath(section, 0)]; } - (UICollectionReusableView *)footerForSection:(NSInteger)section { return [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:genIndexPath(section, 0)]; } - (void)setUpWithStickyHeaders:(BOOL)sticky showHeaderWhenEmpty:(BOOL)showHeaderWhenEmpty { self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:YES topContentInset:0 stretchToEdge:NO]; self.layout.showHeaderWhenEmpty = showHeaderWhenEmpty; [self setUpCollectionViewAndDataSource:kTestFrame]; } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset { [self setUpWithStickyHeaders:sticky topInset:inset stretchToEdge:NO]; } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset testFrame:(CGRect)testFrame { [self setUpWithStickyHeaders:sticky scrollDirection:UICollectionViewScrollDirectionVertical topInset:inset stretchToEdge:NO testFrame:testFrame]; } - (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge { [self setUpWithStickyHeaders:sticky scrollDirection:UICollectionViewScrollDirectionVertical topInset:inset stretchToEdge:stretchToEdge testFrame:kTestFrame]; } - (void)setUpWithStickyHeaders:(BOOL)sticky scrollDirection:(UICollectionViewScrollDirection)scrollDirection topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge testFrame:(CGRect)testFrame { self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky scrollDirection:scrollDirection topContentInset:inset stretchToEdge:stretchToEdge]; [self setUpCollectionViewAndDataSource:testFrame]; } - (void)setUpCollectionViewAndDataSource:(CGRect)testFrame { self.dataSource = [IGLayoutTestDataSource new]; self.collectionView = [[UICollectionView alloc] initWithFrame:testFrame collectionViewLayout:self.layout]; self.collectionView.dataSource = self.dataSource; self.collectionView.delegate = self.dataSource; [self.dataSource configCollectionView:self.collectionView]; } - (void)tearDown { [super tearDown]; self.collectionView = nil; self.layout = nil; self.dataSource = nil; } - (void)prepareWithData:(NSArray *)data { self.dataSource.sections = data; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; } - (void)test_whenApplyingSameBoundsValue_thatLayoutIsntInvalidated { [self setUpWithStickyHeaders:YES topInset:0]; [self prepareWithData:nil]; XCTAssertFalse([self.layout shouldInvalidateLayoutForBoundsChange:self.collectionView.bounds]); } - (void)test_whenApplyingInvalidatedSectionLogic_thatMinimumInvalidatedSectionIsCorrect { [self setUpWithStickyHeaders:YES topInset:0]; [self prepareWithData:nil]; [self.layout didModifySection:NSNotFound]; [self.layout didModifySection:0]; [self.layout didModifySection:NSNotFound]; } - (void)test_whenEmptyData_thatContentSizeZero { [self setUpWithStickyHeaders:YES topInset:0]; [self prepareWithData:nil]; // check so that nil messaging doesn't default size to 0 XCTAssertEqual(self.layout.collectionView, self.collectionView); XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, self.collectionView.contentSize)); } - (void)test_whenSectionDataIsEmpty_thatStickyHeaderStillShow { [self setUpWithStickyHeaders:YES showHeaderWhenEmpty:YES]; [self prepareWithData:@[[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:nil], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:20 footerHeight:0 items:nil], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:30 footerHeight:0 items:nil]]]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 0, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 10, 100, 20); IGAssertEqualFrame([self headerForSection:2].frame, 0, 30, 100, 30); } - (void)test_whenSectionDataIsEmpty_thatStickyHeaderShouldBeHidden { [self setUpWithStickyHeaders:YES showHeaderWhenEmpty:NO]; [self prepareWithData:@[[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}] ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:20 footerHeight:0 items:nil], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:20 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]] ]]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 0, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 0, 0, 0); IGAssertEqualFrame([self headerForSection:2].frame, 0, 20, 100, 20); } - (void)test_whenLayingOutCellsVertically_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO topInset:0]; const CGFloat headerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 120); IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 10, 20, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 40, 85, 20); IGAssertEqualFrame([self headerForSection:1].frame, 10, 75, 85, 10); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 85, 85, 30); } - (void)test_whenLayingOutCellsVertically_withFooterHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO topInset:0 testFrame:CGRectMake(0, 0, 100, 150)]; const CGFloat footerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:0 footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:0 footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 120); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 10, 10, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 30, 85, 20); IGAssertEqualFrame([self footerForSection:0].frame, 10, 50, 85, 10); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 75, 85, 30); IGAssertEqualFrame([self footerForSection:1].frame, 10, 105, 85, 10); } - (void)test_whenLayingOutCellsVertically_withHeaderHeight_withFooterHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO topInset:0 testFrame:CGRectMake(0, 0, 100, 150)]; const CGFloat headerHeight = 10; const CGFloat footerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 140); IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 10, 20, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 40, 85, 20); IGAssertEqualFrame([self footerForSection:0].frame, 10, 60, 85, 10); IGAssertEqualFrame([self headerForSection:1].frame, 10, 85, 85, 10); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 95, 85, 30); IGAssertEqualFrame([self footerForSection:1].frame, 10, 125, 85, 10); } - (void)test_whenLayingOutCellsHorizontally_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO testFrame:kTestFrame]; const CGFloat headerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 30}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.width, 140); IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 10, 85); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 20, 10, 45, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 20, 20, 45, 20); IGAssertEqualFrame([self headerForSection:1].frame, 80, 10, 10, 85); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 90, 10, 45, 30); } - (void)test_whenLayingOutCellsHorizontally_withFooterHeight_withLineSpacing_withInsets_thatFramesCorrect { [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO testFrame:kTestFrame]; const CGFloat footerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:0 footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:0 footerHeight:footerHeight items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {45, 30}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.width, 75); IGAssertEqualFrame([self footerForSection:0].frame, 60, 10, 10, 85); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 10, 10, 45, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 20, 45, 20); IGAssertEqualFrame([self footerForSection:1].frame, 60, 10, 10, 85); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 55, 45, 30); } - (void)test_whenUsingStickyHeaders_withSimulatedScrolling_thatYPositionsAdjusted { [self setUpWithStickyHeaders:YES topInset:10]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 20}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 30}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 30}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 30}], ]], ]]; // scroll header 0 halfway self.collectionView.contentOffset = CGPointMake(0, 5); [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 15, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 50, 100, 10); // scroll header 0 off and 1 up self.collectionView.contentOffset = CGPointMake(0, 45); [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 40, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10); } - (void)test_whenUsingStickyHeaders_withSimulatedHorizontalScrolling_thatXPositionsAdjusted { [self setUpWithStickyHeaders:YES scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:10 stretchToEdge:NO testFrame:kTestFrame]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {20, 100}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {20, 100}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 100}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 100}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 100}], ]], ]]; // scroll header 0 halfway self.collectionView.contentOffset = CGPointMake(5, 0); [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 15, 0, 10, 100); IGAssertEqualFrame([self headerForSection:1].frame, 50, 0, 10, 100); // scroll header 0 off and 1 left self.collectionView.contentOffset = CGPointMake(45, 0); [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 40, 0, 10, 100); IGAssertEqualFrame([self headerForSection:1].frame, 55, 0, 10, 100); } - (void)test_whenAdjustingTopYInset_withVaryingHeaderHeights_thatYPositionsUpdated { [self setUpWithStickyHeaders:YES topInset:10]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 30}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 40}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 50}], ]], ]]; // scroll header 0 off and 1 up self.collectionView.contentOffset = CGPointMake(0, 35); [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 45, 100, 10); self.layout.stickyHeaderYOffset = -10; [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 40, 100, 10); self.layout.stickyHeaderYOffset = 10; [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self headerForSection:0].frame, 0, 30, 100, 10); IGAssertEqualFrame([self headerForSection:1].frame, 0, 55, 100, 10); } - (void)test_whenItemsSmallerThanContainerWidth_with0Insets_with0LineSpacing_with0Interitem_thatItemsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 66); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:2].frame, 66, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 33, 33, 33); } - (void)test_whenItemsSmallerThanContainerWidth_withHalfPointItemSpacing_with0Insets_with0LineSpacing_thatItemsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0.5 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 33); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); const CGRect rect = IGListRectIntegralScaled(CGRectMake(33.5, 0, 33, 33)); IGAssertEqualFrame([self cellForSection:0 item:1].frame, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); IGAssertEqualFrame([self cellForSection:0 item:2].frame, 67, 0, 33, 33); } - (void)test_whenItemsLargerThanContainerHeight_withHorizontalScrolling_with5PointItemSpacing_with0Insets_with10PointLineSpacing_thatItemsBumpToNewColumn { [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO testFrame:kTestFrame]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:10 interitemSpacing:5 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.width, 76); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 38, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:2].frame, 43, 0, 33, 33); } - (void)test_whenSectionsSmallerThanContainerWidth_withVerticalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 33); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 33, 0, 33, 33); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 66, 0, 33, 33); } - (void)test_whenSectionsSmallerThanContainerHeight_withHorizontalScrolling_with0ItemSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameColumn { [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO testFrame:kTestFrame]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.width, 33); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 33, 33, 33); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 0, 66, 33, 33); } - (void)test_whenSectionsSmallerThanContainerWidth_withHalfPointSpacing_with0Insets_with0LineSpacing_thatSectionsFitSameRow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0.5 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0.5 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0.5 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 33); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); const CGRect rect = IGListRectIntegralScaled(CGRectMake(33.5, 0, 33, 33)); IGAssertEqualFrame([self cellForSection:1 item:0].frame, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 67, 0, 33, 33); } - (void)test_whenSectionsSmallerThanContainerWidth_with0ItemSpacing_withMiddleItemHasInsets_with0LineSpacing_thatNextSectionSnapsBelow { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsMake(10, 10, 10, 10) lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {13, 50}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 103); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 43, 10, 13, 50); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 66, 0, 33, 33); IGAssertEqualFrame([self cellForSection:3 item:0].frame, 0, 70, 33, 33); } - (void)test_whenSectionBustingRow_thatNewlineAppliesSectionInset { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsMake(10, 10, 5, 5) lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 50}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 98); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 43, 85, 50); } - (void)test_whenSectionsSmallerThanWidth_withSectionHeader_thatHeaderCausesNewline { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 76); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 43, 33, 33); } - (void)test_whenSectionsSmallerThanHeight_withHorizontalScrolling_withSectionHeader_thatHeaderCausesNewline { [self setUpWithStickyHeaders:NO scrollDirection:UICollectionViewScrollDirectionHorizontal topInset:0 stretchToEdge:NO testFrame:kTestFrame]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.width, 76); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 43, 0, 33, 33); } - (void)test_whenBatchItemUpdates_withHeaderHeight_withLineSpacing_withInsets_thatLayoutCorrectAfterUpdates { [self setUpWithStickyHeaders:NO topInset:0]; const CGFloat headerHeight = 10; const CGFloat lineSpacing = 10; const UIEdgeInsets insets = UIEdgeInsetsMake(10, 10, 5, 5); // making the view bigger so that we can check all cell frames self.collectionView.frame = CGRectMake(0, 0, 100, 400); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 60}], ]], [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 40}], ]], ]]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.collectionView performBatchUpdates:^{ self.dataSource.sections = @[ [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], // reloaded // deleted ]], // moved from section 3 to 1 [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 40}], ]], // deleted section 2 [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 30}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], // inserted ]], // inserted [[IGLayoutTestSection alloc] initWithInsets:insets lineSpacing:lineSpacing interitemSpacing:0 headerHeight:headerHeight footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}], ]], ]; [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:2]]; [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:3]]; [self.collectionView moveSection:3 toSection:1]; [self.collectionView reloadItemsAtIndexPaths:@[genIndexPath(0, 0)]]; [self.collectionView deleteItemsAtIndexPaths:@[genIndexPath(0, 1)]]; [self.collectionView insertItemsAtIndexPaths:@[genIndexPath(2, 1)]]; } completion:^(BOOL finished) { [self.collectionView layoutIfNeeded]; [expectation fulfill]; XCTAssertEqual(self.collectionView.contentSize.height, 260); IGAssertEqualFrame([self headerForSection:0].frame, 10, 10, 85, 10); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 10, 20, 85, 30); IGAssertEqualFrame([self headerForSection:1].frame, 10, 65, 85, 10); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 10, 75, 85, 40); IGAssertEqualFrame([self headerForSection:2].frame, 10, 130, 85, 10); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 10, 140, 85, 30); IGAssertEqualFrame([self cellForSection:2 item:1].frame, 10, 180, 85, 10); IGAssertEqualFrame([self headerForSection:3].frame, 10, 205, 85, 10); IGAssertEqualFrame([self cellForSection:3 item:0].frame, 10, 215, 85, 10); IGAssertEqualFrame([self cellForSection:3 item:1].frame, 10, 235, 85, 20); }]; [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { XCTAssertNil(error); }]; } - (void)test_whenQueryingLayoutAttributes_withLotsOfCells_thatExactFramesFetched { [self setUpWithStickyHeaders:NO topInset:0]; NSMutableArray *items = [NSMutableArray new]; for (NSInteger i = 0; i < 1000; i++) { [items addObject:[[IGLayoutTestItem alloc] initWithSize:(CGSize) {100, 20}]]; } [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:items] ]]; XCTAssertEqual([self.layout layoutAttributesForElementsInRect:CGRectMake(0, 500, 100, 100)].count, 5); XCTAssertEqual([self.layout layoutAttributesForElementsInRect:CGRectMake(0, 0, 100, 1000)].count, 50); XCTAssertEqual([self.layout layoutAttributesForElementsInRect:CGRectMake(0, 250, 100, 100)].count, 6); XCTAssertEqual([self.layout layoutAttributesForElementsInRect:CGRectMake(0, 250, 100, 1)].count, 1); } - (void)test_whenSecondItemDoesntIntersectRect_thatOtherAttributesExist { [self setUpWithStickyHeaders:NO topInset:0]; NSMutableArray *data = [NSMutableArray new]; for (NSInteger i = 0; i < 6; i++) { [data addObject:[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {50, 100}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {50, 10}], ]]]; } [self prepareWithData:data]; NSArray *attributes = [self.layout layoutAttributesForElementsInRect:CGRectMake(0, 50, 100, 100)]; NSArray *paths = [[attributes valueForKeyPath:@"indexPath"] sortedArrayUsingSelector:@selector(compare:)]; NSArray *expectation = @[ genIndexPath(0, 0), genIndexPath(1, 0), genIndexPath(1, 1), ]; // should include 2 of the 100-height items and one of the 10-height XCTAssertEqualObjects(paths, expectation); } - (void)test_whenTwoConsecutiveItemsDontIntersectRect_thatOtherAttributesExist { [self setUpWithStickyHeaders:NO topInset:0]; NSMutableArray *data = [NSMutableArray new]; for (NSInteger i = 0; i < 6; i++) { [data addObject:[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 100}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {30, 10}], ]]]; } [self prepareWithData:data]; NSArray *attributes = [self.layout layoutAttributesForElementsInRect:CGRectMake(0, 50, 100, 100)]; NSArray *paths = [[attributes valueForKeyPath:@"indexPath"] sortedArrayUsingSelector:@selector(compare:)]; NSArray *expectation = @[ genIndexPath(0, 0), genIndexPath(1, 0), genIndexPath(1, 1), genIndexPath(1, 2), ]; // should include 2 of the 100-height items and two of the 10-height XCTAssertEqualObjects(paths, expectation); } - (void)test_whenChangingBoundsSize_withItemsThatNewlineAfterChange_thatLayoutShiftsItems { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {33, 33}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 33); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 33, 0, 33, 33); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 66, 0, 33, 33); // can no longer fit 3 items in one section self.collectionView.frame = CGRectMake(0, 0, 70, 100); [self.collectionView layoutIfNeeded]; XCTAssertEqual(self.collectionView.contentSize.height, 66); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 33, 0, 33, 33); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 0, 33, 33, 33); } - (void)test_whenCollectionViewContentInset_withFullWidthItems_thatItemsPinchedIn { [self setUpWithStickyHeaders:NO topInset:0]; self.collectionView.contentInset = UIEdgeInsetsMake(0, 30, 0, 30); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:10 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {40, 10}], [[IGLayoutTestItem alloc] initWithSize:(CGSize) {40, 20}], ]], ]]; XCTAssertEqual(self.collectionView.contentSize.height, 40); IGAssertEqualFrame([self headerForSection:0].frame, 0, 0, 40, 10); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 10, 40, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 20, 40, 20); } - (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceSmallerThanEpsilon { [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; const CGSize size = CGSizeMake(33, 33); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:size], [[IGLayoutTestItem alloc] initWithSize:size], [[IGLayoutTestItem alloc] initWithSize:size], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:2].frame, 66, 0, 34, 33); } - (void)test_whenItemsAddedWidthSmallerThanWidth_DifferenceBiggerThanEpsilon { [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 33, 33); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 33, 0, 65, 33); } - (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceSmallerThanEpsilon { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(51, 50)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 50, 0, 51, 50); } - (void)test_whenItemsAddedWithBiggerThanWidth_DifferenceBiggerThanEpsilon { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(50, 50)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(52, 50)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 50, 50); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 0, 50, 52, 50); } - (void)test_ { [self setUpWithStickyHeaders:NO topInset:0]; self.collectionView.frame = CGRectMake(0, 0, 414, 736); NSMutableArray *data = [NSMutableArray new]; for (NSInteger i = 0; i < 6; i++) { [data addObject:[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsMake(1, 1, 1, 1) lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:(CGSize) {136, 136}], ]]]; } [self prepareWithData:data]; XCTAssertEqual(self.collectionView.contentSize.height, 276); IGAssertEqualFrame([self cellForSection:0 item:0].frame, 1, 1, 136, 136); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 139, 1, 136, 136); IGAssertEqualFrame([self cellForSection:2 item:0].frame, 277, 1, 136, 136); IGAssertEqualFrame([self cellForSection:3 item:0].frame, 1, 139, 136, 136); IGAssertEqualFrame([self cellForSection:4 item:0].frame, 139, 139, 136, 136); IGAssertEqualFrame([self cellForSection:5 item:0].frame, 277, 139, 136, 136); } - (void)test_whenQueryingAttributes_withSectionOOB_thatReturnsNil { [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], ]], ]]; XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:genIndexPath(4, 0)]); } - (void)test_whenQueryingAttributes_withItemOOB_thatReturnsNil { [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(65, 33)], ]], ]]; XCTAssertNil([self.layout layoutAttributesForItemAtIndexPath:genIndexPath(0, 4)]); } - (void)test_whenQueryingSupplementaryAttributes_withSizeEmpty_thatReturnsNil { [self setUpWithStickyHeaders:NO topInset:0 stretchToEdge:YES]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(33, 33)], ]], ]]; XCTAssertNil([self.layout layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:genIndexPath(0, 0)]); XCTAssertNil([self.layout layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:genIndexPath(0, 0)]); } - (void)test_whenUpdatingSizes_thatLayoutUpdates { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 0, 10, 10); [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(20, 20)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 20, 0, 20, 20); IGAssertEqualFrame([self cellForSection:0 item:2].frame, 40, 0, 20, 20); } - (void)test_whenMarkingASectionAsUpdated_thatLayoutUpdates { [self setUpWithStickyHeaders:NO topInset:0]; [self prepareWithData:@[ [[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:@[ [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)], [[IGLayoutTestItem alloc] initWithSize:CGSizeMake(10, 10)], ]], ]]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 0, 10, 10); [self.layout didModifySection:1]; [self.layout prepareLayout]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10); IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 0, 10, 10); } #pragma mark - Internal debugging - (void)test_withDelegateNameDebugger_thatReturnedNamesAreValid { [self setUpWithStickyHeaders:NO topInset:0]; // Test with the regular delegate XCTAssertTrue([[self.layout _classNameForDelegate:(id)self.collectionView.delegate sectionIndex:0] isEqualToString:@"IGLayoutTestDataSource"]); // Test with a proxy providing a new adapter IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; IGListAdapterProxy *proxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:self.collectionView.delegate scrollViewTarget:nil interceptor:adapter]; XCTAssertNil([self.layout _classNameForDelegate:(id)proxy sectionIndex:0]); // Test with a proxy with an invalid adapter IGListAdapterProxy *invalidProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:self.collectionView.delegate scrollViewTarget:nil interceptor:(id)[NSObject new]]; XCTAssertNil([self.layout _classNameForDelegate:(id)invalidProxy sectionIndex:0]); } - (void)test_withSupplementalViewAttributes_thatOOBErrorsAreHandled { [self setUpWithStickyHeaders:NO topInset:0]; XCTAssertNil([self.layout layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:[NSIndexPath indexPathForItem:10 inSection:10]]); } @end ================================================ FILE: Tests/IGListCollectionViewTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGLayoutTestDataSource.h" #import "IGLayoutTestItem.h" #import "IGLayoutTestSection.h" #import "IGListTestHelpers.h" @interface IGListCollectionViewTests : XCTestCase @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) IGListCollectionView *collectionView; @property (nonatomic, strong) IGLayoutTestDataSource *dataSource; @end @implementation IGListCollectionViewTests - (void)setUp { [super setUp]; const CGRect frame = CGRectMake(0, 0, 100, 100); self.window = [[UIWindow alloc] initWithFrame:frame]; self.dataSource = [IGLayoutTestDataSource new]; self.collectionView = [[IGListCollectionView alloc] initWithFrame:frame]; self.collectionView.dataSource = self.dataSource; self.collectionView.delegate = self.dataSource; [self.window addSubview:self.collectionView]; [self.dataSource configCollectionView:self.collectionView]; } #pragma mark - Reload All - (void)test_whenReloadData_thatEntireLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20); IGAssertEqualFrame([self cellForSection:1 item:0].frame, 20, 0, 10, 10); } #pragma mark - Insert/Delete/Reload/Move - (void)test_whenInsertingSection_thatLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:1]]; // check that section 0 was updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20); // check that section 1 was updated IGAssertEqualFrame([self cellForSection:1 item:0].frame, 20, 0, 10, 10); } - (void)test_whenDeletingSection_thatLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), ]; [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:1]]; // check that section 0 wasn't updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20); } - (void)test_whenReloadingSection_thatLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), ]; [self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:1]]; // check that section 0 was updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 20, 20); // check that section 1 was updated IGAssertEqualFrame([self cellForSection:1 item:0].frame, 20, 0, 20, 20); } - (void)test_whenMoveSection_thatLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(30, 30))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(40, 40))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(30, 30))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), ]; [self.collectionView moveSection:1 toSection:2]; // check that section 0 was updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 40, 40); // check that section 1 was updated IGAssertEqualFrame([self cellForSection:1 item:0].frame, 40, 0, 30, 30); // check that section 2 was updated IGAssertEqualFrame([self cellForSection:2 item:0].frame, 70, 0, 20, 20); } - (void)test_whenMoveItem_thatLayoutPartiallyUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(30, 30))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; NSArray *sections = @[genLayoutTestItem(CGSizeMake(10, 10)), genLayoutTestItem(CGSizeMake(20, 20))]; self.dataSource.sections = @[ genLayoutTestSection(sections), genLayoutTestSection(@[]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(20, 20))]), ]; [self.collectionView moveItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] toIndexPath:[NSIndexPath indexPathForItem:1 inSection:0]]; // check that section 0 wasn't updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10); // check that section 1 was updated IGAssertEqualFrame([self cellForSection:0 item:1].frame, 10, 0, 20, 20); // check that section 2 was updated IGAssertEqualFrame([self cellForSection:2 item:0].frame, 30, 0, 20, 20); } #pragma mark - Batch - (void)test_whenInsertDeleteMoveSection_thatLayoutUpdates { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(1, 1))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(2, 2))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(3, 3))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(4, 4))]), ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeZero)]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(4, 4))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(3, 3))]), genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(5, 5))]), ]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; [self.collectionView performBatchUpdates:^{ [self.collectionView deleteSections:[NSIndexSet indexSetWithIndex:1]]; // deleted (2, 2) [self.collectionView moveSection:3 toSection:1]; // move (4, 4) [self.collectionView insertSections:[NSIndexSet indexSetWithIndex:3]]; // inserted (5, 5) } completion:^(BOOL finished) { [self.collectionView layoutIfNeeded]; [expectation fulfill]; // check that section 0 was updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 0, 0); // check that section 1 was updated IGAssertEqualFrame([self cellForSection:1 item:0].frame, 0, 0, 4, 4); // check that section 2 was updated IGAssertEqualFrame([self cellForSection:2 item:0].frame, 4, 0, 3, 3); // check that section 3 was updated IGAssertEqualFrame([self cellForSection:3 item:0].frame, 7, 0, 5, 5); }]; [self waitForExpectationsWithTimeout:30 handler:^(NSError * _Nullable error) { XCTAssertNil(error); }]; } - (void)test_whenInsertingNilSection_thatExecutionCompletesCleanly { self.dataSource.sections = @[ genLayoutTestSection(@[genLayoutTestItem(CGSizeMake(10, 10))]) ]; [self.collectionView reloadData]; [self.collectionView layoutIfNeeded]; [self.collectionView insertSections:[NSIndexSet indexSet]]; // check that section 0 wasn't updated IGAssertEqualFrame([self cellForSection:0 item:0].frame, 0, 0, 10, 10); } #pragma mark - Helpers - (UICollectionViewCell *)cellForSection:(NSInteger)section item:(NSInteger)item { return [self.collectionView cellForItemAtIndexPath:genIndexPath(section, item)]; } @end ================================================ FILE: Tests/IGListContentInsetTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListAdapterInternal.h" #import "IGListTestHelpers.h" #import "UIScrollView+IGListKit.h" static const CGRect kStackTestFrame = (CGRect){{0.0, 0.0}, {320.0, 480.0}}; @interface IGListContentInsetTests : XCTestCase @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UIViewController *viewController; @property (nonatomic, strong) UICollectionView *collectionView; @end @implementation IGListContentInsetTests - (void)setUp { [super setUp]; self.viewController = [UIViewController new]; IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; self.collectionView = [[UICollectionView alloc] initWithFrame:kStackTestFrame collectionViewLayout:layout]; self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.viewController.view addSubview:self.collectionView]; self.adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:self.viewController]; self.adapter.dataSource = self; self.adapter.collectionView = self.collectionView; self.window = [[UIWindow alloc] initWithFrame:kStackTestFrame]; self.window.rootViewController = self.viewController; } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; self.adapter = nil; self.viewController = nil; self.collectionView = nil; } - (void) testCollectionViewContentInset { const UIEdgeInsets inset = UIEdgeInsetsMake(10, 0, 10, 0); self.collectionView.contentInset = inset; IGAssertEqualInsets(self.collectionView.ig_contentInset, inset.top, inset.left, inset.bottom, inset.right); id context = self.adapter; IGAssertEqualInsets(context.adjustedContainerInset, inset.top, inset.left, inset.bottom, inset.right); } #pragma mark - IGListAdapterDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return @[]; } - (IGListSectionController *) listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return nil; } - (UIView *) emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/IGListDebugDescriptionTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapter+DebugDescription.h" #import "IGListAdapterUpdater+DebugDescription.h" #import "IGListAdapterUpdaterInternal.h" #import "IGListAdapterInternal.h" #import "IGListBatchUpdateData+DebugDescription.h" #import "IGListMoveIndexInternal.h" #import "IGListMoveIndexPathINternal.h" #import "IGListTestAdapterDataSource.h" #import "IGListTestCase.h" #import "IGListUpdateTransactable.h" #import "IGTestDelegateDataSource.h" #import "IGTestObject.h" @interface IGListAdapterUpdater (DebugDescriptionTests) - (void)setTransaction:(id)transaction; @end @interface IGListBindingSectionController (DebugDescriptionTests) - (void)setViewModels:(NSArray> *)viewModels; @end @interface IGListDebugDescriptionTests : IGListTestCase @end @implementation IGListDebugDescriptionTests - (void)setUp { self.dataSource = [IGListTestAdapterDataSource new]; [super setUp]; } - (void)test_withListAdapter_thatDebugDescriptionIsValid { self.dataSource.objects = @[@1, @2, @3]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil]; adapter.collectionView = self.collectionView; adapter.dataSource = self.dataSource; [adapter.registeredCellIdentifiers addObject:@"IGCellIdentifier"]; [adapter.registeredNibNames addObject:@"IGCellNibName"]; [adapter.registeredSupplementaryViewIdentifiers addObject:@"IGSupplementaryViewIdentifier"]; [adapter.registeredSupplementaryViewNibNames addObject:@"IGSupplementaryNibName"]; IGListBindingSectionController *bindingSectionController = [[IGListBindingSectionController alloc] init]; bindingSectionController.viewModels = @[[[IGTestObject alloc] initWithKey:@"Key" value:@(1)]]; adapter.previousSectionMap = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [adapter.previousSectionMap updateWithObjects:@[@1] sectionControllers:@[bindingSectionController]]; XCTAssertTrue(adapter.debugDescriptionLines.count > 0); XCTAssertTrue(adapter.debugDescription.length > 0); } - (void)test_withListAdapterUpdater_thatDebugDescriptionIsValid { id transactionMock = [OCMockObject mockForProtocol:@protocol(IGListUpdateTransactable)]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; [updater setTransaction:transactionMock]; [[[transactionMock expect] andReturnValue:@(IGListBatchUpdateStateIdle)] state]; XCTAssertTrue(updater.debugDescriptionLines.count > 0); [[[transactionMock expect] andReturnValue:@(IGListBatchUpdateStateQueuedBatchUpdate)] state]; XCTAssertTrue(updater.debugDescriptionLines.count > 0); [[[transactionMock expect] andReturnValue:@(IGListBatchUpdateStateExecutingBatchUpdateBlock)] state]; XCTAssertTrue(updater.debugDescriptionLines.count > 0); [[[transactionMock expect] andReturnValue:@(IGListBatchUpdateStateExecutedBatchUpdateBlock)] state]; XCTAssertTrue(updater.debugDescriptionLines.count > 0); } - (void)test_withBatchUpdateData_thatDebugDescriptionIsValid { NSMutableIndexSet *insertSections = [NSMutableIndexSet indexSet]; [insertSections addIndex:0]; [insertSections addIndex:1]; NSIndexSet *deleteSections = [NSIndexSet indexSetWithIndex:5]; IGListMoveIndex *moveSections = [[IGListMoveIndex alloc] initWithFrom:3 to:4]; NSIndexPath *insertIndexPaths = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *deleteIndexPaths = [NSIndexPath indexPathForItem:0 inSection:0]; IGListMoveIndexPath *moveIndexPaths = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:0 inSection:6] to:[NSIndexPath indexPathForItem:1 inSection:6]]; IGListBatchUpdateData *data = [[IGListBatchUpdateData alloc] initWithInsertSections:insertSections deleteSections:deleteSections moveSections:[NSSet setWithObject:moveSections] insertIndexPaths:@[insertIndexPaths] deleteIndexPaths:@[deleteIndexPaths] updateIndexPaths:@[] moveIndexPaths:@[moveIndexPaths]]; XCTAssertTrue(data.debugDescriptionLines.count > 0); } @end ================================================ FILE: Tests/IGListDebuggerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListDebugger.h" #import "IGListTestAdapterDataSource.h" @interface IGListDebuggerTests : XCTestCase @end @implementation IGListDebuggerTests - (void)test_whenSearchingAdapterInstances_thatCorrectCountReturned { // purge any leftover tracking [IGListDebugger clear]; UIViewController *controller = [UIViewController new]; UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 320, 480) collectionViewLayout:[UICollectionViewFlowLayout new]]; IGListTestAdapterDataSource *dataSource = [IGListTestAdapterDataSource new]; dataSource.objects = @[@1, @2, @3]; IGListAdapter *adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:nil workingRangeSize:0]; adapter1.collectionView = collectionView; adapter1.dataSource = dataSource; IGListAdapter *adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:controller workingRangeSize:2]; adapter2.collectionView = collectionView; adapter2.dataSource = dataSource; IGListAdapter *adapter3 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:controller workingRangeSize:2]; adapter3.collectionView = collectionView; adapter3.dataSource = dataSource; [collectionView setNeedsLayout]; [collectionView layoutIfNeeded]; NSArray *descriptions = [IGListDebugger adapterDescriptions]; XCTAssertEqual(descriptions.count, 3); XCTAssertTrue([[IGListDebugger dump] length] > 0); } @end ================================================ FILE: Tests/IGListDiffDescriptionStringTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListMoveIndexInternal.h" #import "IGListMoveIndexPathInternal.h" #import "IGListIndexSetResultInternal.h" #import "IGListIndexPathResultInternal.h" @interface IGListDiffDescriptionStringTests : XCTestCase @end @implementation IGListDiffDescriptionStringTests - (void)test_withBatchUpdateData_thatDescriptionStringIsValid { NSMutableIndexSet *insertSections = [NSMutableIndexSet indexSet]; [insertSections addIndex:0]; [insertSections addIndex:1]; NSIndexSet *deleteSections = [NSIndexSet indexSetWithIndex:5]; IGListMoveIndex *moveSections = [[IGListMoveIndex alloc] initWithFrom:3 to:4]; NSIndexPath *insertIndexPaths = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *deleteIndexPaths = [NSIndexPath indexPathForItem:0 inSection:0]; IGListMoveIndexPath *moveIndexPaths = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:0 inSection:6] to:[NSIndexPath indexPathForItem:1 inSection:6]]; IGListBatchUpdateData *result = [[IGListBatchUpdateData alloc] initWithInsertSections:insertSections deleteSections:deleteSections moveSections:[NSSet setWithObject:moveSections] insertIndexPaths:@[insertIndexPaths] deleteIndexPaths:@[deleteIndexPaths] updateIndexPaths:@[] moveIndexPaths:@[moveIndexPaths]]; NSString *expectedDescription = [NSString stringWithFormat:@"", result]; XCTAssertTrue([result.description isEqualToString:expectedDescription]); } - (void)test_withIndexPathResult_thatDescriptionStringIsValid { NSArray *inserts = @[[NSIndexPath indexPathForItem:0 inSection:0]]; NSArray *deletes = @[ [NSIndexPath indexPathForItem:0 inSection:1], [NSIndexPath indexPathForItem:1 inSection:1] ]; NSArray *updates = @[ [NSIndexPath indexPathForItem:1 inSection:0] ]; NSArray *moves = @[ [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:1 inSection:3] to:[NSIndexPath indexPathForItem:2 inSection:3]], [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:4 inSection:3] to:[NSIndexPath indexPathForItem:3 inSection:3]] ]; IGListIndexPathResult *result = [[IGListIndexPathResult alloc] initWithInserts:inserts deletes:deletes updates:updates moves:moves oldIndexPathMap:[NSMapTable mapTableWithKeyOptions:0 valueOptions:0] newIndexPathMap:[NSMapTable mapTableWithKeyOptions:0 valueOptions:0]]; NSString *expectedDescription = [NSString stringWithFormat:@"", result]; XCTAssertTrue([result.description isEqualToString:expectedDescription]); } - (void)test_withIndexSetResult_thatDescriptionStringIsValid { NSMutableIndexSet *inserts = [NSMutableIndexSet indexSet]; [inserts addIndex:0]; [inserts addIndex:1]; NSMutableIndexSet *deletes = [NSMutableIndexSet indexSet]; [deletes addIndex:3]; NSMutableIndexSet *updates = [NSMutableIndexSet indexSet]; [updates addIndex:4]; [updates addIndex:5]; [updates addIndex:6]; NSArray *moves = @[ [[IGListMoveIndex alloc] initWithFrom:9 to:10], [[IGListMoveIndex alloc] initWithFrom:11 to:12] ]; IGListIndexSetResult *result = [[IGListIndexSetResult alloc] initWithInserts:inserts deletes:deletes updates:updates moves:moves oldIndexMap:[NSMapTable mapTableWithKeyOptions:0 valueOptions:0] newIndexMap:[NSMapTable mapTableWithKeyOptions:0 valueOptions:0]]; NSString *expectedDescription = [NSString stringWithFormat:@"", result]; XCTAssertTrue([result.description isEqualToString:expectedDescription]); } - (void)test_withMoveIndex_thatDescriptionStringIsValid { IGListMoveIndex *moveIndex = [[IGListMoveIndex alloc] initWithFrom:3 to:4]; NSString *expectedDescription = [NSString stringWithFormat:@"", moveIndex]; XCTAssertTrue([moveIndex.description isEqualToString:expectedDescription]); } - (void)test_withMoveIndexPath_thatDescriptionStringIsValid { NSIndexPath *from = [NSIndexPath indexPathForItem:1 inSection:1]; NSIndexPath *to = [NSIndexPath indexPathForItem:3 inSection:1]; IGListMoveIndexPath *moveIndexPath = [[IGListMoveIndexPath alloc] initWithFrom:from to:to]; NSString *expectedDescription = [NSString stringWithFormat:@" {length = 2, path = 1 - 1}; " "to: {length = 2, path = 1 - 3};>", moveIndexPath, from, to]; XCTAssertTrue([moveIndexPath.description isEqualToString:expectedDescription]); } @end ================================================ FILE: Tests/IGListDiffResultTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListMoveIndexInternal.h" #import "IGListMoveIndexPathInternal.h" @interface IGListDiffResultTests : XCTestCase @end @implementation IGListDiffResultTests - (void)test_whenDuplicateMoves_withIndexPaths_thatSetCountCorrect { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:4 inSection:2] to:[NSIndexPath indexPathForItem:7 inSection:5]]; IGListMoveIndexPath *other = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:4 inSection:2] to:[NSIndexPath indexPathForItem:7 inSection:5]]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 1); } - (void)test_whenUniqueMoves_withIndexPaths_whenFlippedIndexPaths_thatSetCountCorrect { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:4 inSection:2] to:[NSIndexPath indexPathForItem:7 inSection:5]]; IGListMoveIndexPath *other = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:7 inSection:5] to:[NSIndexPath indexPathForItem:4 inSection:4]]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 2); } - (void)test_whenUniqueMoves_withIndexPaths_thatSetCountCorrect { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:4 inSection:2] to:[NSIndexPath indexPathForItem:7 inSection:5]]; IGListMoveIndexPath *other = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:10 inSection:2] to:[NSIndexPath indexPathForItem:6 inSection:11]]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 2); } - (void)test_whenDuplicateMoves_withIndexes_thatSetCountCorrect { IGListMoveIndex *move = [[IGListMoveIndex alloc] initWithFrom:4 to:2]; IGListMoveIndex *other = [[IGListMoveIndex alloc] initWithFrom:4 to:2]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 1); } - (void)test_whenUniqueMoves_withIndexes_whenFlippedIndexes_thatSetCountCorrect { IGListMoveIndex *move = [[IGListMoveIndex alloc] initWithFrom:4 to:2]; IGListMoveIndex *other = [[IGListMoveIndex alloc] initWithFrom:2 to:4]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 2); } - (void)test_whenUniqueMoves_withIndexes_thatSetCountCorrect { IGListMoveIndex *move = [[IGListMoveIndex alloc] initWithFrom:4 to:2]; IGListMoveIndex *other = [[IGListMoveIndex alloc] initWithFrom:5 to:7]; NSSet *set = [NSSet setWithObjects:move, other, nil]; XCTAssertEqual(set.count, 2); } - (void)test_whenComparingMovePointers_withIndexPaths_thatEqual { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath new] to:[NSIndexPath new]]; XCTAssertTrue([move isEqual:move]); } - (void)test_whenComparingMovePointers_withIndexes_thatEqual { IGListMoveIndex *move = [[IGListMoveIndex alloc] initWithFrom:1 to:1]; XCTAssertTrue([move isEqual:move]); } - (void)test_whenComparingMoves_withIndexPaths_withNonMove_thatNotEqual { IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath new] to:[NSIndexPath new]]; XCTAssertFalse([move isEqual:[NSObject new]]); } - (void)test_whenComparingMoves_withIndexes_withNonMove_thatNotEqual { IGListMoveIndex *move = [[IGListMoveIndex alloc] initWithFrom:1 to:1]; XCTAssertFalse([move isEqual:[NSObject new]]); } - (void)test_whenSortingMoves_withIndexPaths_thatSorted { NSArray *moves = @[ [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:2 inSection:2] to:[NSIndexPath indexPathForItem:3 inSection:3]], [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:6 inSection:3] to:[NSIndexPath indexPathForItem:7 inSection:4]], [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:0 inSection:1] to:[NSIndexPath indexPathForItem:1 inSection:5]], [[IGListMoveIndexPath alloc] initWithFrom:[NSIndexPath indexPathForItem:2 inSection:2] to:[NSIndexPath indexPathForItem:3 inSection:3]], ]; NSArray *expected = @[moves[2], moves[0], moves[3], moves[1]]; NSArray *sorted = [moves sortedArrayUsingSelector:@selector(compare:)]; XCTAssertEqualObjects(sorted, expected); } - (void)test_whenSortingMoves_withIndexes_thatSorted { NSArray *moves = @[ [[IGListMoveIndex alloc] initWithFrom:2 to:2], [[IGListMoveIndex alloc] initWithFrom:3 to:2], [[IGListMoveIndex alloc] initWithFrom:1 to:2], [[IGListMoveIndex alloc] initWithFrom:2 to:2], ]; NSArray *expected = @[moves[2], moves[0], moves[3], moves[1]]; NSArray *sorted = [moves sortedArrayUsingSelector:@selector(compare:)]; XCTAssertEqualObjects(sorted, expected); } @end ================================================ FILE: Tests/IGListDiffSwiftTests.swift ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import XCTest import IGListDiffKit class SwiftClass: ListDiffable { let id: Int let value: String init(id: Int, value: String) { self.id = id self.value = value } @objc func diffIdentifier() -> NSObjectProtocol { return NSNumber(value: id) } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? SwiftClass else { return false } return id == object.id && value == object.value } } class IGDiffingSwiftTests: XCTestCase { func testDiffingStrings() { let o: [NSString] = ["a", "b", "c"] let n: [NSString] = ["a", "c", "d"] let result = ListDiff(oldArray: o, newArray: n, option: .equality) XCTAssertEqual(result.deletes, IndexSet(integer: 1)) XCTAssertEqual(result.inserts, IndexSet(integer: 2)) XCTAssertEqual(result.moves.count, 0) XCTAssertEqual(result.updates.count, 0) } func testDiffingNumbers() { let o: [NSNumber] = [0, 1, 2] let n: [NSNumber] = [0, 2, 4] let result = ListDiff(oldArray: o, newArray: n, option: .equality) XCTAssertEqual(result.deletes, IndexSet(integer: 1)) XCTAssertEqual(result.inserts, IndexSet(integer: 2)) XCTAssertEqual(result.moves.count, 0) XCTAssertEqual(result.updates.count, 0) } func testDiffingSwiftClass() { let o = [SwiftClass(id: 0, value: "a"), SwiftClass(id: 1, value: "b"), SwiftClass(id: 2, value: "c")] let n = [SwiftClass(id: 0, value: "a"), SwiftClass(id: 2, value: "c"), SwiftClass(id: 4, value: "d")] let result = ListDiff(oldArray: o, newArray: n, option: .equality) XCTAssertEqual(result.deletes, IndexSet(integer: 1)) XCTAssertEqual(result.inserts, IndexSet(integer: 2)) XCTAssertEqual(result.moves.count, 0) XCTAssertEqual(result.updates.count, 0) } func testDiffingSwiftClassPointerComparison() { let o = [SwiftClass(id: 0, value: "a"), SwiftClass(id: 1, value: "b"), SwiftClass(id: 2, value: "c")] let n = [SwiftClass(id: 0, value: "a"), SwiftClass(id: 2, value: "c"), SwiftClass(id: 4, value: "d")] let result = ListDiff(oldArray: o, newArray: n, option: .pointerPersonality) XCTAssertEqual(result.deletes, IndexSet(integer: 1)) XCTAssertEqual(result.inserts, IndexSet(integer: 2)) XCTAssertEqual(result.moves.count, 0) XCTAssertEqual(result.updates.count, 2) } func testDiffingSwiftClassWithUpdates() { let o = [SwiftClass(id: 0, value: "a"), SwiftClass(id: 1, value: "b"), SwiftClass(id: 2, value: "c")] let n = [SwiftClass(id: 0, value: "b"), SwiftClass(id: 1, value: "b"), SwiftClass(id: 2, value: "b")] let result = ListDiff(oldArray: o, newArray: n, option: .equality) XCTAssertEqual(result.deletes.count, 0) XCTAssertEqual(result.inserts.count, 0) XCTAssertEqual(result.moves.count, 0) XCTAssertEqual(result.updates.count, 2) } } ================================================ FILE: Tests/IGListDiffTests.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import // Exposed for internal benchmark tests @interface IGListDiffTests : XCTestCase @end ================================================ FILE: Tests/IGListDiffTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListDiffTests.h" #import #import #import #import "IGListIndexSetResultInternal.h" #import "IGListMoveIndexInternal.h" #import "IGListMoveIndexPathInternal.h" #import "IGTestObject.h" #define genIndexPath(i, s) [NSIndexPath indexPathForItem:i inSection:s] #define IGAssertContains(collection, object) do {\ id haystack = collection; id needle = object; \ XCTAssertTrue([haystack containsObject:needle], @"%@ does not contain %@", haystack, needle); \ } while(0) static NSIndexSet *indexSetWithIndexes(NSArray *indexes) { NSMutableIndexSet *indexset = [NSMutableIndexSet new]; for (NSNumber *i in indexes) { [indexset addIndex:i.integerValue]; } return indexset; } static NSArray *sorted(NSArray *arr) { return [arr sortedArrayUsingSelector:@selector(compare:)]; } @implementation IGListDiffTests - (void)test_whenDiffingEmptyArrays_thatResultHasNoChanges { NSArray *o = @[]; NSArray *n = @[]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertFalse([result hasChanges]); } - (void)test_whenDiffingFromEmptyArray_thatResultHasChanges { NSArray *o = @[]; NSArray *n = @[@1]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqualObjects(result.inserts, [NSIndexSet indexSetWithIndex:0]); XCTAssertEqual([result changeCount], 1); } - (void)test_whenDiffingToEmptyArray_thatResultHasChanges { NSArray *o = @[@1]; NSArray *n = @[]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqualObjects(result.deletes, [NSIndexSet indexSetWithIndex:0]); XCTAssertEqual([result changeCount], 1); } - (void)test_whenDiffingToEmptyArray_thatOldIndexPathsAreCorrect { NSArray *o = @[@1, @2]; NSArray *n = @[]; IGListIndexPathResult *result = IGListDiffPaths(0, 1, o, n, IGListDiffEquality); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@1], [NSIndexPath indexPathForItem:0 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@2], [NSIndexPath indexPathForItem:1 inSection:0]); } - (void)test_whenDiffingFromEmptyArray_thatNewIndexPathsAreCorrect { NSArray *o = @[]; NSArray *n = @[@1, @2]; IGListIndexPathResult *result = IGListDiffPaths(0, 1, o, n, IGListDiffEquality); XCTAssertEqualObjects([result newIndexPathForIdentifier:@1], [NSIndexPath indexPathForItem:0 inSection:1]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@2], [NSIndexPath indexPathForItem:1 inSection:1]); } - (void)test_whenSwappingObjects_thatResultHasMoves { NSArray *o = @[@1, @2]; NSArray *n = @[@2, @1]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); NSArray *expected = @[ [[IGListMoveIndex alloc] initWithFrom:0 to:1], [[IGListMoveIndex alloc] initWithFrom:1 to:0], ]; NSArray *sortedMoves = sorted(result.moves); XCTAssertEqualObjects(sortedMoves, expected); XCTAssertEqual([result changeCount], 2); } - (void)test_whenMovingObjectsTogether_thatResultHasMoves { // "trick" is having multiple @3s NSArray *o = @[@1, @2, @3, @3, @4]; NSArray *n = @[@2, @3, @1, @3, @4]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); IGAssertContains(result.moves, [[IGListMoveIndex alloc] initWithFrom:1 to:0]); IGAssertContains(result.moves, [[IGListMoveIndex alloc] initWithFrom:0 to:2]); } - (void)test_whenDiffingWordsFromPaper_withIndexPaths_thatDeletesMatchPaper { // http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL&CFID=529464736&CFTOKEN=43088172 NSString *oString = @"much writing is like snow , a mass of long words and phrases falls upon the relevant facts covering up the details ."; NSString *nString = @"a mass of latin words falls upon the relevant facts like soft snow , covering up the details ."; NSArray *o = [oString componentsSeparatedByString:@" "]; NSArray *n = [nString componentsSeparatedByString:@" "]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffEquality); NSArray *expected = @[genIndexPath(0, 0), genIndexPath(1, 0), genIndexPath(2, 0), genIndexPath(9, 0), genIndexPath(11, 0), genIndexPath(12, 0)]; XCTAssertEqualObjects(result.deletes, expected); } - (void)test_whenDiffingWordsFromPaper_withIndexPaths_thatInsertsMatchPaper { // http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL&CFID=529464736&CFTOKEN=43088172 NSString *oString = @"much writing is like snow , a mass of long words and phrases falls upon the relevant facts covering up the details ."; NSString *nString = @"a mass of latin words falls upon the relevant facts like soft snow , covering up the details ."; NSArray *o = [oString componentsSeparatedByString:@" "]; NSArray *n = [nString componentsSeparatedByString:@" "]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffEquality); NSArray *expected = @[genIndexPath(3, 0), genIndexPath(11, 0)]; XCTAssertEqualObjects(result.inserts, expected); } - (void)test_whenSwappingObjects_withIndexPaths_thatResultHasMoves { NSArray *o = @[@1, @2, @3, @4]; NSArray *n = @[@2, @4, @5, @3]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffEquality); NSArray *expected = @[ [[IGListMoveIndexPath alloc] initWithFrom:genIndexPath(2, 0) to:genIndexPath(3, 0)], [[IGListMoveIndexPath alloc] initWithFrom:genIndexPath(3, 0) to:genIndexPath(1, 0)], ]; NSArray *sortedMoves = sorted(result.moves); XCTAssertEqualObjects(sortedMoves, expected); } - (void)test_whenObjectEqualityChanges_thatResultHasUpdates { NSArray *o = @[ genTestObject(@"0", @0), genTestObject(@"1", @1), genTestObject(@"2", @2), ]; NSArray *n = @[ genTestObject(@"0", @0), genTestObject(@"1", @3), // value updated from @1 to @3 genTestObject(@"2", @2), ]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffEquality); NSArray *expected = @[genIndexPath(1, 0)]; XCTAssertEqualObjects(result.updates, expected); } - (void)test_whenDiffingWordsFromPaper_thatInsertsMatchPaper { // http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL&CFID=529464736&CFTOKEN=43088172 NSString *oString = @"much writing is like snow , a mass of long words and phrases falls upon the relevant facts covering up the details ."; NSString *nString = @"a mass of latin words falls upon the relevant facts like soft snow , covering up the details ."; NSArray *o = [oString componentsSeparatedByString:@" "]; NSArray *n = [nString componentsSeparatedByString:@" "]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); NSIndexSet *expectedInserts = indexSetWithIndexes(@[@3, @11]); XCTAssertEqualObjects(result.inserts, expectedInserts); } - (void)test_whenDiffingWordsFromPaper_thatDeletesMatchPaper { // http://dl.acm.org/citation.cfm?id=359467&dl=ACM&coll=DL&CFID=529464736&CFTOKEN=43088172 NSString *oString = @"much writing is like snow , a mass of long words and phrases falls upon the relevant facts covering up the details ."; NSString *nString = @"a mass of latin words falls upon the relevant facts like soft snow , covering up the details ."; NSArray *o = [oString componentsSeparatedByString:@" "]; NSArray *n = [nString componentsSeparatedByString:@" "]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); NSIndexSet *expectedDeletes = indexSetWithIndexes(@[@0, @1, @2, @9, @11, @12]); XCTAssertEqualObjects(result.deletes, expectedDeletes); } - (void)test_whenDeletingItems_withInserts_withMoves_thatResultHasInsertsMovesAndDeletes { NSArray *o = @[@0, @1, @2, @3, @4, @5, @6, @7, @8]; NSArray *n = @[@0, @2, @3, @4, @7, @6, @9, @5, @10]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); NSIndexSet *expectedDeletes = indexSetWithIndexes(@[@1, @8]); NSIndexSet *expectedInserts = indexSetWithIndexes(@[@6, @8]); NSArray *expectedMoves = @[ [[IGListMoveIndex alloc] initWithFrom:5 to:7], [[IGListMoveIndex alloc] initWithFrom:7 to:4], ]; NSArray *sortedMoves = sorted(result.moves); XCTAssertEqualObjects(result.deletes, expectedDeletes); XCTAssertEqualObjects(result.inserts, expectedInserts); XCTAssertEqualObjects(sortedMoves, expectedMoves); } - (void)test_whenMovingItems_withEqualityChanges_thatResultsHasMovesAndUpdates { NSArray *o = @[ genTestObject(@"0", @0), genTestObject(@"1", @1), genTestObject(@"2", @2), ]; // objects 0 and 2 are swapped and object at original index 2 has its data changed to @3 NSArray *n = @[ genTestObject(@"2", @3), genTestObject(@"1", @1), genTestObject(@"0", @0), ]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); NSArray *expectedMoves = @[ [[IGListMoveIndex alloc] initWithFrom:0 to:2], [[IGListMoveIndex alloc] initWithFrom:2 to:0], ]; NSIndexSet *expectedUpdates = [NSIndexSet indexSetWithIndex:2]; NSArray *sortedMoves = sorted(result.moves); XCTAssertEqualObjects(result.updates, expectedUpdates); XCTAssertEqualObjects(sortedMoves, expectedMoves); } - (void)test_whenDiffingPointers_withObjectCopy_thatResultHasUpdate { NSArray *o = @[ genTestObject(@"0", @0), genTestObject(@"1", @1), genTestObject(@"2", @2), ]; NSArray *n = @[ o[0], [o[1] copy], // new pointer o[2], ]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffPointerPersonality); NSArray *expected = @[genIndexPath(1, 0)]; XCTAssertEqualObjects(result.updates, expected); } - (void)test_whenDiffingPointers_withSameObjects_thatResultHasNoChanges { NSArray *o = @[ genTestObject(@"0", @0), genTestObject(@"1", @1), genTestObject(@"2", @2), ]; NSArray *n = [o copy]; IGListIndexPathResult *result = IGListDiffPaths(0, 0, o, n, IGListDiffPointerPersonality); XCTAssertFalse([result hasChanges]); } - (void)test_whenDeletingObjects_withArrayOfEqualObjects_thatChangeCountMatches { NSArray *o = @[@"dog", @"dog", @"dog", @"dog"]; NSArray *n = @[@"dog", @"dog"]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); // there is a "flaw" in the algorithm that cannot detect bulk ops when they are all the same object // confirm that the results are at least correct XCTAssertEqual(o.count + result.inserts.count - result.deletes.count, 2); } - (void)test_whenInsertingObjects_withArrayOfEqualObjects_thatChangeCountMatches { NSArray *o = @[@"dog", @"dog"]; NSArray *n = @[@"dog", @"dog", @"dog", @"dog"]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); // there is a "flaw" in the algorithm that cannot detect bulk ops when they are all the same object // confirm that the results are at least correct XCTAssertEqual(o.count + result.inserts.count - result.deletes.count, 4); } - (void)test_whenInsertingObject_withOldArrayHavingMultiples_thatChangeCountMatches { NSArray *o = @[@(NSNotFound), @(NSNotFound), @(NSNotFound), @49, @33, @"cat", @"cat", @0, @14]; NSMutableArray *n = [o mutableCopy]; [n insertObject:@"cat" atIndex:5]; // 3 cats in a row IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqual(o.count + result.inserts.count - result.deletes.count, 10); } - (void)test_whenMovingDuplicateObjects_thatChangeCountMatches { NSArray *o = @[@1, @20, @14, @(NSNotFound), @"cat", @(NSNotFound), @4, @"dog", @"cat", @"cat", @"fish", @(NSNotFound), @"fish", @(NSNotFound)]; NSArray *n = @[@1, @28, @14, @"cat", @"cat", @4, @"dog", o[3], @"cat", @"fish", o[11], @"fish", o[13]]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqual(o.count + result.inserts.count - result.deletes.count, n.count); } - (void)test_whenDiffingDuplicatesAtTail_withDuplicateAtHead_thatResultHasNoChanges { NSArray *o = @[@"cat", @1, @2, @3, @"cat"]; NSArray *n = @[@"cat", @1, @2, @3, @"cat"]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertFalse([result hasChanges]); } - (void)test_whenDuplicateObjects_thatMovesAreUnique { NSArray *o = @[@"cat", @(NSNotFound), @"dog", @"dog", @(NSNotFound), @(NSNotFound), @"cat", @65]; NSArray *n = @[@"cat", o[1], @"dog", o[4], @"dog", o[5], @"cat", @"cat", @"fish", @65]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqual([[NSSet setWithArray:[[result moves] valueForKeyPath:@"from"]] count], [result.moves count]); } - (void)test_whenMovingObjectShiftsOthers_thatMovesContainRequiredMoves { NSArray *o = @[@1, @2, @3, @4, @5, @6, @7]; NSArray *n = @[@1, @4, @5, @2, @3, @6, @7]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); IGAssertContains(result.moves, [[IGListMoveIndex alloc] initWithFrom:3 to:1]); IGAssertContains(result.moves, [[IGListMoveIndex alloc] initWithFrom:1 to:3]); } - (void)test_whenDiffing_thatOldIndexesMatch { NSArray *o = @[@1, @2, @3, @4, @5, @6, @7]; NSArray *n = @[@2, @9, @3, @1, @5, @6, @8]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqual([result oldIndexForIdentifier:@1], 0); XCTAssertEqual([result oldIndexForIdentifier:@2], 1); XCTAssertEqual([result oldIndexForIdentifier:@3], 2); XCTAssertEqual([result oldIndexForIdentifier:@4], 3); XCTAssertEqual([result oldIndexForIdentifier:@5], 4); XCTAssertEqual([result oldIndexForIdentifier:@6], 5); XCTAssertEqual([result oldIndexForIdentifier:@7], 6); XCTAssertEqual([result oldIndexForIdentifier:@8], NSNotFound); XCTAssertEqual([result oldIndexForIdentifier:@9], NSNotFound); } - (void)test_whenDiffing_thatNewIndexesMatch { NSArray *o = @[@1, @2, @3, @4, @5, @6, @7]; NSArray *n = @[@2, @9, @3, @1, @5, @6, @8]; IGListIndexSetResult *result = IGListDiff(o, n, IGListDiffEquality); XCTAssertEqual([result newIndexForIdentifier:@1], 3); XCTAssertEqual([result newIndexForIdentifier:@2], 0); XCTAssertEqual([result newIndexForIdentifier:@3], 2); XCTAssertEqual([result newIndexForIdentifier:@4], NSNotFound); XCTAssertEqual([result newIndexForIdentifier:@5], 4); XCTAssertEqual([result newIndexForIdentifier:@6], 5); XCTAssertEqual([result newIndexForIdentifier:@7], NSNotFound); XCTAssertEqual([result newIndexForIdentifier:@8], 6); XCTAssertEqual([result newIndexForIdentifier:@9], 1); } - (void)test_whenDiffing_thatOldIndexPathsMatch { NSArray *o = @[@1, @2, @3, @4, @5, @6, @7]; NSArray *n = @[@2, @9, @3, @1, @5, @6, @8]; IGListIndexPathResult *result = IGListDiffPaths(0, 1, o, n, IGListDiffEquality); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@1], [NSIndexPath indexPathForItem:0 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@2], [NSIndexPath indexPathForItem:1 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@3], [NSIndexPath indexPathForItem:2 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@4], [NSIndexPath indexPathForItem:3 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@5], [NSIndexPath indexPathForItem:4 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@6], [NSIndexPath indexPathForItem:5 inSection:0]); XCTAssertEqualObjects([result oldIndexPathForIdentifier:@7], [NSIndexPath indexPathForItem:6 inSection:0]); XCTAssertNil([result oldIndexPathForIdentifier:@8]); XCTAssertNil([result oldIndexPathForIdentifier:@9]); } - (void)test_whenDiffing_thatNewIndexPathsMatch { NSArray *o = @[@1, @2, @3, @4, @5, @6, @7]; NSArray *n = @[@2, @9, @3, @1, @5, @6, @8]; IGListIndexPathResult *result = IGListDiffPaths(0, 1, o, n, IGListDiffEquality); XCTAssertEqualObjects([result newIndexPathForIdentifier:@1], [NSIndexPath indexPathForItem:3 inSection:1]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@2], [NSIndexPath indexPathForItem:0 inSection:1]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@3], [NSIndexPath indexPathForItem:2 inSection:1]); XCTAssertNil([result newIndexPathForIdentifier:@4]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@5], [NSIndexPath indexPathForItem:4 inSection:1]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@6], [NSIndexPath indexPathForItem:5 inSection:1]); XCTAssertNil([result newIndexPathForIdentifier:@7]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@8], [NSIndexPath indexPathForItem:6 inSection:1]); XCTAssertEqualObjects([result newIndexPathForIdentifier:@9], [NSIndexPath indexPathForItem:1 inSection:1]); } - (void)test_whenDiffing_withBatchUpdateResult_thatIndexesMatch { NSArray *o = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1), genTestObject(@5, @1), genTestObject(@6, @1), ]; NSArray *n = @[ // deleted genTestObject(@2, @2), // updated genTestObject(@5, @1), // moved genTestObject(@4, @1), genTestObject(@7, @1), // inserted genTestObject(@6, @2), // updated genTestObject(@3, @2), // moved+updated ]; IGListIndexSetResult *result = [IGListDiff(o, n, IGListDiffEquality) resultForBatchUpdates]; XCTAssertEqual(result.updates.count, 0); NSArray *expectedMoves = @[ [[IGListMoveIndex alloc] initWithFrom:4 to:1] ]; XCTAssertEqualObjects(result.moves, expectedMoves); NSMutableIndexSet *expectedDeletes = [NSMutableIndexSet indexSetWithIndex:0]; [expectedDeletes addIndex:1]; [expectedDeletes addIndex:2]; [expectedDeletes addIndex:5]; XCTAssertEqualObjects(result.deletes, expectedDeletes); NSMutableIndexSet *expectedInserts = [NSMutableIndexSet indexSetWithIndex:0]; [expectedInserts addIndex:3]; [expectedInserts addIndex:4]; [expectedInserts addIndex:5]; XCTAssertEqualObjects(result.inserts, expectedInserts); } - (void)test_whenDiffing_withBatchUpdateResult_thatIndexPathsMatch { NSArray *o = @[ genTestObject(@1, @1), genTestObject(@2, @1), genTestObject(@3, @1), genTestObject(@4, @1), genTestObject(@5, @1), genTestObject(@6, @1), ]; NSArray *n = @[ // deleted genTestObject(@2, @2), // updated genTestObject(@5, @1), // moved genTestObject(@4, @1), genTestObject(@7, @1), // inserted genTestObject(@6, @2), // updated genTestObject(@3, @2), // moved+updated ]; IGListIndexPathResult *result = [IGListDiffPaths(0, 1, o, n, IGListDiffEquality) resultForBatchUpdates]; XCTAssertEqual(result.updates.count, 0); NSArray *expectedMoves = @[ [[IGListMoveIndexPath alloc] initWithFrom:genIndexPath(4, 0) to:genIndexPath(1, 1)] ]; XCTAssertEqualObjects(result.moves, expectedMoves); NSArray *expectedDeletes = @[genIndexPath(0, 0), genIndexPath(1, 0), genIndexPath(2, 0), genIndexPath(5, 0)]; XCTAssertEqualObjects(sorted(result.deletes), expectedDeletes); NSArray *expectedInserts = @[genIndexPath(0, 1), genIndexPath(3, 1), genIndexPath(4, 1), genIndexPath(5, 1)]; XCTAssertEqualObjects(sorted(result.inserts), expectedInserts); } @end ================================================ FILE: Tests/IGListDisplayHandlerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapterInternal.h" #import "IGListDisplayHandler.h" #import "IGListTestAdapterDataSource.h" #import "IGListTestSection.h" @interface IGListDisplayHandlerTests : XCTestCase @property (nonatomic, strong) IGListDisplayHandler *displayHandler; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) id mockDisplayDelegate; @property (nonatomic, strong) id mockAdapterDelegate; @property (nonatomic, strong) id mockAdapterDataSource; @property (nonatomic, strong) IGListTestSection *list; @property (nonatomic, strong) id object; @end @implementation IGListDisplayHandlerTests - (void)setUp { [super setUp]; self.list = [[IGListTestSection alloc] init]; self.object = [[NSObject alloc] init]; self.displayHandler = [[IGListDisplayHandler alloc] init]; UICollectionView *collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; self.mockAdapterDataSource = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterDataSource)]; IGListAdapterUpdater *updater = [IGListAdapterUpdater new]; self.adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; self.adapter.collectionView = collectionView; self.adapter.dataSource = self.mockAdapterDataSource; self.mockDisplayDelegate = [OCMockObject mockForProtocol:@protocol(IGListDisplayDelegate)]; self.mockAdapterDelegate = [OCMockObject mockForProtocol:@protocol(IGListAdapterDelegate)]; } - (void)tearDown { [super tearDown]; self.list.displayDelegate = nil; self.adapter.delegate = nil; } - (void)test_whenDisplayingFirstCell_thatDisplayHandlerReceivesEvent { NSIndexPath *path = [NSIndexPath new]; UICollectionViewCell *cell = [UICollectionViewCell new]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:self.list]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:self.list cell:cell atIndex:path.item]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter willDisplayObject:self.object atIndex:path.section]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter willDisplayObject:self.object cell:cell atIndexPath:path]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [self.displayHandler willDisplayCell:cell forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:path]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenDisplayingSecondCell_thatDisplayHandlerReceivesEvent { // simulate first cell appearing in the collection view NSIndexPath *firstPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.displayHandler willDisplayCell:[UICollectionViewCell new] forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:firstPath]; NSIndexPath *nextPath = [NSIndexPath indexPathForItem:1 inSection:0]; UICollectionViewCell *cell = [UICollectionViewCell new]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:self.list cell:cell atIndex:nextPath.item]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter willDisplaySectionController:self.list]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter willDisplayObject:self.object cell:cell atIndexPath:nextPath]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter willDisplayObject:self.object atIndex:firstPath.section]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [self.displayHandler willDisplayCell:cell forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:nextPath]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenEndDisplayingSecondToLastCell_thatDisplayHandlerReceivesEvent { // simulate first cell appearing in the collection view NSIndexPath *firstPath = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cellOne = [UICollectionViewCell new]; UICollectionViewCell *cellTwo = [UICollectionViewCell new]; [self.displayHandler willDisplayCell:cellOne forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:firstPath]; NSIndexPath *nextPath = [NSIndexPath indexPathForItem:1 inSection:0]; [self.displayHandler willDisplayCell:cellTwo forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:nextPath]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:cellOne atIndex:firstPath.item]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:firstPath.section]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object cell:cellOne atIndexPath: firstPath]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [self.displayHandler didEndDisplayingCell:cellOne forListAdapter:self.adapter sectionController:self.list indexPath:firstPath]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenEndDisplayingLastCell_thatDisplayHandlerReceivesEvent { // simulate first cell appearing then disappearing in the collection view NSIndexPath *firstPath = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [UICollectionViewCell new]; [self.displayHandler willDisplayCell:cell forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:firstPath]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:cell atIndex:firstPath.item]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:firstPath.section]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object cell:cell atIndexPath:firstPath]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [self.displayHandler didEndDisplayingCell:cell forListAdapter:self.adapter sectionController:self.list indexPath:firstPath]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenEndDisplayingCell_withCellNeverDisplayed_thatDisplayHandlerReceivesNoEvent { //simulate a cell received didEndDisplay when it didn't receive willDisplay. OS 7 issue only NSIndexPath *firstPath = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [UICollectionViewCell new]; // all following methods shouldn't be called. [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:cell atIndex:firstPath.item]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:firstPath.section]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [self.displayHandler didEndDisplayingCell:cell forListAdapter:self.adapter sectionController:self.list indexPath:firstPath]; } - (void)test_whenEndDisplayingCell_withEndDisplayTwice_thatDisplayHandlerReceivesOneEvent { //simulate a cell received didEndDisplay twice but willDisplay once. OS 7 issue only NSIndexPath *firstPath = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cell = [UICollectionViewCell new]; [self.displayHandler willDisplayCell:cell forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:firstPath]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:cell atIndex:firstPath.item]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:firstPath.section]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object cell:cell atIndexPath:firstPath]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:cell atIndex:firstPath.item]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:firstPath.section]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; //first call [self.displayHandler didEndDisplayingCell:cell forListAdapter:self.adapter sectionController:self.list indexPath:firstPath]; //second call [self.displayHandler didEndDisplayingCell:cell forListAdapter:self.adapter sectionController:self.list indexPath:firstPath]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenCellInserted_withDisplayedCellExistingAtPath_thatDisplayHandlerReceivesCorrectParams { // simulate first cell appearing in the collection view NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionViewCell *cellOne = [UICollectionViewCell new]; // display the "old" cell/object [self.displayHandler willDisplayCell:cellOne forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:path]; // simulate a new object being inserted into the index path of the old section IGListTestSection *anotherList = [IGListTestSection new]; id anotherObject = [NSObject new]; UICollectionViewCell *anotherCell = [UICollectionViewCell new]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:anotherList]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:anotherList cell:anotherCell atIndex:path.item]; anotherList.displayDelegate = self.mockDisplayDelegate; [self.displayHandler willDisplayCell:anotherCell forListAdapter:self.adapter sectionController:anotherList object:anotherObject indexPath:path]; [self.mockDisplayDelegate verify]; } - (void)test_whenWillDisplaySupplementaryView_withCellDisplayedAfter_thatDisplayHandlerReceivesOneEvent { NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionReusableView *view = [UICollectionReusableView new]; UICollectionViewCell *cell = [UICollectionViewCell new]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:self.list]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter willDisplayObject:self.object atIndex:path.section]; [self.displayHandler willDisplaySupplementaryView:view forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:path]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter willDisplaySectionController:self.list cell:cell atIndex:path.item]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter willDisplayObject:self.object atIndex:path.item]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter willDisplaySectionController:self.list]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter willDisplayObject:self.object cell:cell atIndexPath:path]; [self.displayHandler willDisplayCell:cell forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:path]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } - (void)test_whenEndDisplayingSupplementaryView_withEndDisplayingTwice_thatDisplayHandlerReceivesOneEvent { NSIndexPath *path = [NSIndexPath indexPathForItem:0 inSection:0]; UICollectionReusableView *view = [UICollectionReusableView new]; [self.displayHandler willDisplaySupplementaryView:view forListAdapter:self.adapter sectionController:self.list object:self.object indexPath:path]; [[self.mockDisplayDelegate expect] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockAdapterDelegate expect] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:path.section]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list]; [[self.mockDisplayDelegate reject] listAdapter:self.adapter didEndDisplayingSectionController:self.list cell:[OCMArg any] atIndex:path.item]; [[self.mockAdapterDelegate reject] listAdapter:self.adapter didEndDisplayingObject:self.object atIndex:path.section]; self.list.displayDelegate = self.mockDisplayDelegate; self.adapter.delegate = self.mockAdapterDelegate; //first call [self.displayHandler didEndDisplayingSupplementaryView:view forListAdapter:self.adapter sectionController:self.list indexPath:path]; //second call [self.displayHandler didEndDisplayingSupplementaryView:view forListAdapter:self.adapter sectionController:self.list indexPath:path]; [self.mockDisplayDelegate verify]; [self.mockAdapterDelegate verify]; } @end ================================================ FILE: Tests/IGListGenericSectionControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListGenericSectionControllerTests : XCTestCase @end @implementation IGListGenericSectionControllerTests - (void)test_whenUpdatingToObject_thatSameObjectIsStored { IGListGenericSectionController *controller = [IGListGenericSectionController new]; NSString *foo = @"foo"; [controller didUpdateToObject:foo]; XCTAssertEqual(controller.object, foo); } @end ================================================ FILE: Tests/IGListInteractiveMovingTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGTestObject.h" #import "IGListTestCase.h" #import "IGTestDelegateDataSource.h" #import "UICollectionViewLayout+InteractiveReordering.h" @interface UICollectionViewLayout (Tests) - (void)ig_invalidateAccessoryElementsWithSupplementaryIndexPaths:(NSDictionary *> *)supplementaryIndexPaths decorationIndexPaths:(NSDictionary *> *)decorationIndexPaths inContext:(UICollectionViewLayoutInvalidationContext *)context; @end @interface IGListInteractiveMovingTests : IGListTestCase @end @implementation IGListInteractiveMovingTests - (void)setUp { self.workingRangeSize = 2; self.dataSource = [IGTestDelegateDataSource new]; [super setUp]; } - (void)test_withDetachedLayout_whenQueryingForInteractiveMovingItem_thatOriginalIndexPathIsReturned { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:1 inSection:1]; NSIndexPath *targetIndexPath = [layout targetIndexPathForInteractivelyMovingItem:indexPath withPosition:CGPointMake(100, 100)]; XCTAssertEqual(indexPath.item, targetIndexPath.item); XCTAssertEqual(indexPath.section, targetIndexPath.section); } - (void)test_withDetachedLayout_thatCleanupInvalidationContextExitsEarly { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForItem:1 inSection:1]; NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:1 inSection:1]; UICollectionViewLayoutInvalidationContext *context = [layout invalidationContextForInteractivelyMovingItems:@[targetIndexPath] withTargetPosition:CGPointZero previousIndexPaths:@[sourceIndexPath] previousPosition:CGPointZero]; XCTAssertTrue(context.invalidatedItemIndexPaths.count > 0); } - (void)test_whenCollectionViewIsSet_thatTargetIndexPathIsValid { [self setupWithObjects:@[genTestObject(@1, @2)]]; UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:1 inSection:1]; NSIndexPath *targetIndexPath = [layout targetIndexPathForInteractivelyMovingItem:indexPath withPosition:CGPointMake(100, 100)]; XCTAssertEqual(indexPath.item, targetIndexPath.item); XCTAssertEqual(indexPath.section, targetIndexPath.section); } - (void)test_whenCollectionViewIsSet_thatInvalidationContextForInteractivelyMovingItemsPasses { [self setupWithObjects:@[genTestObject(@1, @2), genTestObject(@4, @5)]]; UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForItem:1 inSection:1]; NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:1 inSection:1]; UICollectionViewLayoutInvalidationContext *context = [layout invalidationContextForInteractivelyMovingItems:@[targetIndexPath] withTargetPosition:CGPointZero previousIndexPaths:@[sourceIndexPath] previousPosition:CGPointZero]; XCTAssertTrue(context.invalidatedItemIndexPaths.count > 0); } - (void)test_whenCollectionViewIsSet_andIndexPathIsInsideBounds_thatValidationContextForEndingInteractiveMovementOfItemsToFinalIndexPathsPasses { [self setupWithObjects:@[genTestObject(@1, @2), genTestObject(@4, @5), genTestObject(@6, @7)]]; UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; UICollectionViewLayoutInvalidationContext *context = [layout invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:@[sourceIndexPath] previousIndexPaths:@[targetIndexPath] movementCancelled:NO]; XCTAssertTrue(context.invalidatedItemIndexPaths.count > 0); } - (void)test_whenCollectionViewIsSet_andIndexPathIsOutOfBounds_thatValidationContextForEndingInteractiveMovementOfItemsToFinalIndexPathsPasses { [self setupWithObjects:@[genTestObject(@1, @2), genTestObject(@4, @5), genTestObject(@6, @7)]]; UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForItem:8 inSection:2]; NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:8 inSection:2]; UICollectionViewLayoutInvalidationContext *context = [layout invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:@[sourceIndexPath] previousIndexPaths:@[targetIndexPath] movementCancelled:NO]; XCTAssertTrue(context.invalidatedItemIndexPaths.count == 0); } - (void)test_whenCollectionViewIsSetWithBaseLayout_andIndexPathIsOutOfBounds_thatValidationContextForEndingInteractiveMovementOfItemsToFinalIndexPathsPasses { [self setupWithObjects:@[genTestObject(@1, @2), genTestObject(@4, @5), genTestObject(@6, @7)]]; UICollectionViewLayout *layout = [UICollectionViewLayout new]; [layout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self.adapter]; self.collectionView.collectionViewLayout = layout; NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForItem:8 inSection:2]; NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:8 inSection:2]; UICollectionViewLayoutInvalidationContext *context = [layout invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:@[sourceIndexPath] previousIndexPaths:@[targetIndexPath] movementCancelled:NO]; XCTAssertTrue(context.invalidatedItemIndexPaths.count == 0); } - (void)test_withInvalidationContext_thatSupplementaryAndDecorationIndexPathsAreInvalidated { UICollectionViewLayout *layout = [UICollectionViewLayout new]; NSDictionary *supplementaryDictionary = @{@"supplementary": @[[NSIndexPath indexPathForItem:1 inSection:1]]}; NSDictionary *decorationDictionary = @{@"decoration": @[[NSIndexPath indexPathForItem:2 inSection:2]]}; id contextMock = [OCMockObject mockForClass:[UICollectionViewLayoutInvalidationContext class]]; [[contextMock expect] invalidateSupplementaryElementsOfKind:@"supplementary" atIndexPaths:supplementaryDictionary[@"supplementary"]]; [[contextMock expect] invalidateDecorationElementsOfKind:@"decoration" atIndexPaths:decorationDictionary[@"decoration"]]; [layout ig_invalidateAccessoryElementsWithSupplementaryIndexPaths:supplementaryDictionary decorationIndexPaths:decorationDictionary inContext:contextMock]; [contextMock verify]; } @end ================================================ FILE: Tests/IGListItemUpdatesCollectorTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListItemUpdatesCollector.h" #import "IGListMoveIndexPathInternal.h" #import "IGListReloadIndexPath.h" @interface IGListItemUpdatesCollectorTests : XCTestCase @end @implementation IGListItemUpdatesCollectorTests #pragma mark - Initialization - (void)test_whenInitialized_thatCollectionsAreEmpty { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; XCTAssertEqual(collector.sectionReloads.count, 0); XCTAssertEqual(collector.itemInserts.count, 0); XCTAssertEqual(collector.itemDeletes.count, 0); XCTAssertEqual(collector.itemMoves.count, 0); XCTAssertEqual(collector.itemReloads.count, 0); } #pragma mark - hasChanges - (void)test_whenEmpty_thatHasChangesIsFalse { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; XCTAssertFalse([collector hasChanges]); } - (void)test_whenHasSectionReloads_thatHasChangesIsTrue { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; [collector.sectionReloads addIndex:0]; XCTAssertTrue([collector hasChanges]); } - (void)test_whenHasItemInserts_thatHasChangesIsTrue { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; [collector.itemInserts addObject:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue([collector hasChanges]); } - (void)test_whenHasItemDeletes_thatHasChangesIsTrue { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; [collector.itemDeletes addObject:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue([collector hasChanges]); } - (void)test_whenHasItemMoves_thatHasChangesIsTrue { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *to = [NSIndexPath indexPathForItem:1 inSection:0]; IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to]; [collector.itemMoves addObject:move]; XCTAssertTrue([collector hasChanges]); } - (void)test_whenHasItemReloads_thatHasChangesIsTrue { IGListItemUpdatesCollector *collector = [[IGListItemUpdatesCollector alloc] init]; NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *to = [NSIndexPath indexPathForItem:0 inSection:0]; IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:from toIndexPath:to]; [collector.itemReloads addObject:reload]; XCTAssertTrue([collector hasChanges]); } @end ================================================ FILE: Tests/IGListKitTests-Bridging-Header.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import ================================================ FILE: Tests/IGListPerformDiffTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListPerformDiff.h" #import "IGListTransitionData.h" #import "IGListViewVisibilityTrackerInternal.h" @interface IGListPerformDiffTests : XCTestCase @end @implementation IGListPerformDiffTests - (void)test_whenPerformDiff_withNilCompletion_thatReturnsEarly { IGListTransitionData *data = [[IGListTransitionData alloc] initFromObjects:@[@1] toObjects:@[@2] toSectionControllers:@[]]; IGListAdaptiveDiffingExperimentConfig config = { .enabled = YES, .higherQOSEnabled = NO, .maxItemCountToRunOnMain = 0, .lowerPriorityWhenViewNotVisible = NO }; // Bypass nonnull check by using a variable IGListDiffExecutorCompletion completion = nil; // Should not crash when completion is nil IGListPerformDiffWithData(data, nil, YES, config, completion); } - (void)test_whenPerformDiff_withViewNotVisibleState_thatUsesLowerPriorityQueue { // Create a view not in any window UIView *view = [[UIView alloc] init]; // Get the tracker and set it up so it returns NotVisible (not NotVisibleEarly) IGListViewVisibilityTracker *tracker = IGListViewVisibilityTrackerAttachedOnView(view); tracker.comparedDateOverride = [tracker.dateCreated dateByAddingTimeInterval:tracker.earlyTimeInterval + 1]; // Verify the tracker returns NotVisible XCTAssertEqual(tracker.state, IGListViewVisibilityStateNotVisible); IGListTransitionData *data = [[IGListTransitionData alloc] initFromObjects:@[@1, @2, @3, @4, @5, @6] toObjects:@[@2, @3, @4, @5, @6, @7] toSectionControllers:@[]]; IGListAdaptiveDiffingExperimentConfig config = { .enabled = YES, .higherQOSEnabled = NO, .maxItemCountToRunOnMain = 0, .lowerPriorityWhenViewNotVisible = YES }; XCTestExpectation *expectation = [self expectationWithDescription:@"Diff completed"]; IGListPerformDiffWithData(data, view, YES, config, ^(IGListIndexSetResult *result, BOOL onBackground) { XCTAssertNotNil(result); XCTAssertTrue(onBackground); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:30 handler:nil]; } @end ================================================ FILE: Tests/IGListReloadDataUpdaterTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListAdapterInternal.h" #import "IGListTestAdapterDataSource.h" #import "IGListTestAdapterReorderingDataSource.h" #import "IGListTestSection.h" #import "IGTestReorderableSection.h" #import "UICollectionViewLayout+InteractiveReordering.h" @interface IGReloadDataUpdaterTests : XCTestCase @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) IGListTestAdapterDataSource *dataSource; @property (nonatomic, strong) UICollectionViewFlowLayout *layout; @property (nonatomic, strong) UIWindow *window; @end @implementation IGReloadDataUpdaterTests - (void)setUp { [super setUp]; // minimum line spacing, item size, and minimum interim spacing are all set in IGListTestSection self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; self.layout = [[UICollectionViewFlowLayout alloc] init]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.bounds collectionViewLayout:self.layout]; [self.window addSubview:self.collectionView]; // syncronous reloads so we dont have to do expectations or other nonsense IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; self.dataSource = [[IGListTestAdapterDataSource alloc] init]; self.adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self.dataSource; } - (void)test_whenCompletionBlockExists_thatBlockExecuted { __block BOOL executed = NO; self.dataSource.objects = @[@0, @1, @2]; [self.adapter reloadDataWithCompletion:^(BOOL finished) { executed = YES; }]; XCTAssertTrue(executed); } - (void)test_thatDataUpdateBlockStateIsAvailable { XCTAssertTrue(!self.adapter.updater.isInDataUpdateBlock); } - (void)test_whenCallingWillCrash_thatMethodIsNoOp { // The IGListReloadDataUpdater implementation of willCrashWithCollectionView: is a no-op // This test ensures the method exists and doesn't crash when called IGListReloadDataUpdater *updater = (IGListReloadDataUpdater *)self.adapter.updater; XCTAssertNoThrow([updater willCrashWithCollectionView:self.collectionView sectionControllerClass:[NSObject class]]); } - (void)test_whenInsertingIntoContext_thatCollectionViewUpdated { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListTestSection *section = [self.adapter sectionControllerForObject:@2]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { section.items = 3; [batchContext insertInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 3); } - (void)test_whenDeletingFromContext_thatCollectionViewUpdated { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListTestSection *section = [self.adapter sectionControllerForObject:@2]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { section.items = 1; [batchContext deleteInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); } - (void)test_whenReloadingInContext_thatCollectionViewUpdated { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListTestSection *section = [self.adapter sectionControllerForObject:@2]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext reloadInSectionController:section atIndexes:[NSIndexSet indexSetWithIndex:0]]; } completion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); } - (void)test_whenPerformingUpdate_thatCompletionExecuted { __block BOOL executed = NO; self.dataSource.objects = @[@0, @1, @2]; [self.adapter performUpdatesAnimated:NO completion:^(BOOL finished) { executed = YES; }]; XCTAssertTrue(executed); } - (void)test_whenMovingFromContext_thatCollectionViewUpdated { self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); IGListTestSection *section = [self.adapter sectionControllerForObject:@2]; [section.collectionContext performBatchAnimated:YES updates:^(id batchContext) { [batchContext moveInSectionController:section fromIndex:0 toIndex:1]; } completion:nil]; XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 2); } - (NSIndexPath *)interpretedIndexPathFromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { UICollectionViewLayout *layout = self.collectionView.collectionViewLayout; NSIndexPath *updatedIndexPath = [layout updatedTargetForInteractivelyMovingItem:fromIndexPath toIndexPath:toIndexPath adapter:self.adapter]; if (!updatedIndexPath) { return toIndexPath; } return updatedIndexPath; } - (void)test_whenSectionIsInteractivelyReordered_thatIndexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1, @2]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; IGTestReorderableSection *section0 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; IGTestReorderableSection *section1 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:1]; IGTestReorderableSection *section2 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:2]; section0.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section1.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section2.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:@[@0]]; section2.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath, *interpretedPath; // move the last section into the second position, dropping into the end of the first section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section0, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section1, [self.adapter sectionControllerForSection:2]); // move the last section into the first position, dropping into the start of the first section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section1, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section0, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:2]); // move the first section into the middle position, dropping into the start of the last section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:2]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section0, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section1, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:2]); // move the first section into the last position, dropping into the end of the last section fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:2]; interpretedPath = [self interpretedIndexPathFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:interpretedPath]; XCTAssertEqual(section1, [self.adapter sectionControllerForSection:0]); XCTAssertEqual(section2, [self.adapter sectionControllerForSection:1]); XCTAssertEqual(section0, [self.adapter sectionControllerForSection:2]); } - (void)test_whenItemsInSectionAreInteractivelyReordered_thatIndexesUpdateCorrectly { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSArray *sectionObjects = @[@0, @1, @2]; IGTestReorderableSection *section = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:sectionObjects]; section.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath; // move the last item into the first position fromIndexPath = [NSIndexPath indexPathForItem:2 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; XCTAssertEqual(sectionObjects[2], section.sectionObject.objects[0]); XCTAssertEqual(sectionObjects[0], section.sectionObject.objects[1]); XCTAssertEqual(sectionObjects[1], section.sectionObject.objects[2]); // move the last item into the middle position fromIndexPath = [NSIndexPath indexPathForItem:2 inSection:0]; toIndexPath = [NSIndexPath indexPathForItem:1 inSection:0]; [self.adapter collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; XCTAssertEqual(sectionObjects[2], section.sectionObject.objects[0]); XCTAssertEqual(sectionObjects[1], section.sectionObject.objects[1]); XCTAssertEqual(sectionObjects[0], section.sectionObject.objects[2]); } - (void)test_whenItemsAreInteractivelyReorderedAcrossSections_thatIndexesRevertToOriginalState { IGListTestAdapterReorderingDataSource *dataSource = [IGListTestAdapterReorderingDataSource new]; dataSource.objects = @[@0, @1]; self.adapter.dataSource = dataSource; self.adapter.moveDelegate = dataSource; NSArray *section0Objects = @[@0, @1, @2]; NSArray *section1Objects = @[@3, @4, @5]; IGTestReorderableSection *section0 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:0]; section0.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:section0Objects]; IGTestReorderableSection *section1 = (IGTestReorderableSection *)[self.adapter sectionControllerForSection:1]; section1.sectionObject = [IGTestReorderableSectionObject sectionWithObjects:section1Objects]; section1.isReorderable = YES; [self.adapter performUpdatesAnimated:NO completion:nil]; NSIndexPath *fromIndexPath, *toIndexPath; // move an item from section 1 to section 0 and check that they are reverted fromIndexPath = [NSIndexPath indexPathForItem:0 inSection:1]; toIndexPath = [NSIndexPath indexPathForItem:0 inSection:0]; [self.collectionView performBatchUpdates:^{ [self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; [self.collectionView.dataSource collectionView:self.collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; } completion:nil]; XCTAssertEqual(section0Objects[0], section0.sectionObject.objects[0]); XCTAssertEqual(section0Objects[1], section0.sectionObject.objects[1]); XCTAssertEqual(section0Objects[2], section0.sectionObject.objects[2]); XCTAssertEqual(section1Objects[0], section1.sectionObject.objects[0]); XCTAssertEqual(section1Objects[1], section1.sectionObject.objects[1]); XCTAssertEqual(section1Objects[2], section1.sectionObject.objects[2]); } @end ================================================ FILE: Tests/IGListSectionControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListSectionControllerTests : XCTestCase @end @implementation IGListSectionControllerTests - (void)test_withBaseSectionContoller_thatDefaultValuesAreCorrect { NSObject *object = [NSObject new]; IGListSectionController *sectionController = [[IGListSectionController alloc] init]; XCTAssertNotNil(sectionController); XCTAssertEqual([sectionController numberOfItems], 1); XCTAssertTrue(CGSizeEqualToSize([sectionController sizeForItemAtIndex:0], CGSizeZero)); XCTAssertTrue([sectionController shouldSelectItemAtIndex:0]); XCTAssertTrue([sectionController shouldDeselectItemAtIndex:0]); XCTAssertFalse([sectionController canMoveItemAtIndex:0]); [sectionController didUpdateToObject:object]; [sectionController didSelectItemAtIndex:0]; [sectionController didDeselectItemAtIndex:0]; [sectionController didHighlightItemAtIndex:0]; [sectionController didUnhighlightItemAtIndex:0]; @try { [sectionController cellForItemAtIndex:0]; } @catch (NSException *exception) {} @try { [sectionController moveObjectFromIndex:0 toIndex:1]; } @catch (NSException *exception) {} } - (void)test_whenCreatedOutsideDataSource_thatCollectionContextIsNil { // Creating a section controller directly (not through the data source) should result in // a nil collectionContext since the thread context stack is empty. This covers line 61. // Clear the thread context stack to ensure we're testing the "outside data source" path // This is needed because other tests may have left context on the stack NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; [threadDictionary removeObjectForKey:@"kIGListSectionControllerThreadKey"]; IGListSectionController *sectionController = [[IGListSectionController alloc] init]; XCTAssertNil(sectionController.collectionContext); XCTAssertNil(sectionController.viewController); } #if !TARGET_OS_TV - (void)test_whenCallingContextMenuConfiguration_thatDefaultReturnsNil { // The default implementation of contextMenuConfigurationForItemAtIndex:point: returns nil // Context menus are not available on tvOS IGListSectionController *sectionController = [[IGListSectionController alloc] init]; if (@available(iOS 13.0, *)) { UIContextMenuConfiguration *config = [sectionController contextMenuConfigurationForItemAtIndex:0 point:CGPointZero]; XCTAssertNil(config); } } #endif @end ================================================ FILE: Tests/IGListSectionMapTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGTestDiffingObject.h" #import "IGListSectionMap.h" #import "IGListTestSection.h" #import "IGTestObject.h" @interface IGListSectionMapTests : XCTestCase @end @implementation IGListSectionMapTests - (void)test_whenUpdatingItems_thatArraysAreEqual { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqualObjects(objects, map.objects); } - (void)test_whenUpdatingItems_thatSectionControllersAreMappedForSection { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqualObjects([map sectionControllerForSection:1], sectionControllers[1]); } - (void)test_whenUpdatingItems_thatSectionControllersAreMappedForItem { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqual([map sectionControllerForObject:objects[1]], sectionControllers[1]); } - (void)test_whenUpdatingItems_thatSectionsAreMappedForSectionController { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqual([map sectionForSectionController:sectionControllers[1]], 1); } - (void)test_whenUpdatingItems_withUnknownItem_thatSectionControllerIsNil { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertNil([map sectionControllerForObject:@4]); } - (void)test_whenUpdatingItems_withSectionController_thatSectionIsNotFound { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqual([map sectionForSectionController:[IGListTestSection new]], NSNotFound); } - (void)test_whenEnumeratingMap_withStopFlagSet_thatEnumerationEndsEarly { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; __block NSInteger counter = 0; [map enumerateUsingBlock:^(id item, IGListSectionController * sectionController, NSInteger section, BOOL *stop) { counter++; *stop = section == 1; }]; XCTAssertEqual(counter, 2); } - (void)test_whenAccessingOOBSection_thatNilIsReturned { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertNil([map objectForSection:4]); } - (void)test_whenUpdatingItems_thatSectionControllerIndexesAreUpdated { NSArray *objects = @[@0, @1, @2]; IGListTestSection *one = [IGListTestSection new]; XCTAssertEqual(one.section, NSNotFound); NSArray *sectionControllers = @[[IGListTestSection new], one, [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertEqual(one.section, 1); XCTAssertFalse(one.isFirstSection); } - (void)test_whenQueryingItems_thatNilReturnsNotFound { IGTestDiffingObject *object = [IGTestDiffingObject new]; object = nil; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; XCTAssertEqual([map sectionForObject:object], NSNotFound); } - (void)test_whenAccessingNegativeSection_thatNilIsReturned { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; XCTAssertNil([map objectForSection:-1]); } - (void)test_whenUpdatingWithDifferentObjectCounts_thatValidationHandlesMismatch { // First update with 3 objects NSArray *objects1 = @[@0, @1, @2]; NSArray *sectionControllers1 = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects1 sectionControllers:sectionControllers1]; // Update with different count - this exercises the count mismatch path in validation NSArray *objects2 = @[@3, @4]; NSArray *sectionControllers2 = @[[IGListTestSection new], [IGListTestSection new]]; [map updateWithObjects:objects2 sectionControllers:sectionControllers2]; XCTAssertEqual(map.objects.count, 2); } - (void)test_whenCopyingMap_thatCopyIsIndependent { NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; IGListSectionMap *copy = [map copy]; XCTAssertEqualObjects(map.objects, copy.objects); XCTAssertNotEqual(map, copy); } - (void)test_whenValidatingWithMismatchedSnapshotCount_thatValidationReturnsEarly { // Set up the map with objects NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; // Use KVC to set a mismatched snapshot (different count) to trigger line 183 // mObjects has 3 items, snapshot has 2 - validation should return early // Must use NSMutableArray since _updateAllDiffIdentifiers calls removeAllObjects [map setValue:[@[@"id1", @"id2"] mutableCopy] forKey:@"diffIdentifiersSnapshot"]; // Trigger validation by updating - the validation should handle the count mismatch gracefully NSArray *newObjects = @[@3, @4, @5]; NSArray *newSectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; [map updateWithObjects:newObjects sectionControllers:newSectionControllers]; XCTAssertEqual(map.objects.count, 3); } - (void)test_whenUpdatingObjectNotInMap_thatValidationHandlesInvalidSection { // Set up the map with objects NSArray *objects = @[@0, @1, @2]; NSArray *sectionControllers = @[[IGListTestSection new], [IGListTestSection new], [IGListTestSection new]]; IGListSectionMap *map = [[IGListSectionMap alloc] initWithMapTable:[NSMapTable strongToStrongObjectsMapTable]]; [map updateWithObjects:objects sectionControllers:sectionControllers]; // Calling updateObject: with an object not in the map will: // 1. Get section = NSNotFound from sectionForObject: // 2. Call _validateDiffIdentifierAtSection: which hits line 187 (section >= mObjects.count) // 3. Then crash when trying to access mObjects[NSNotFound] // The validation at line 187 handles the invalid section gracefully before the crash XCTAssertThrows([map updateObject:@99]); } @end ================================================ FILE: Tests/IGListSingleNibItemControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListTestCase.h" #import "IGTestCell.h" #import "IGTestSingleNibItemDataSource.h" #if !TARGET_OS_TV @interface IGListSingleNibSectionControllerTests : IGListTestCase @end @implementation IGListSingleNibSectionControllerTests - (void)setUp { self.dataSource = [IGTestSingleNibItemDataSource new]; self.frame = CGRectMake(0, 0, 100, 1000); [super setUp]; } - (void)test_whenDisplayingCollectionView_thatSectionsHaveOneItem { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1); } - (void)test_whenDisplayingCollectionView_thatCellsAreConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); XCTAssertEqualObjects(cell3.label.text, @"Baz"); } - (void)test_whenDisplayingCollectionView_thatCellsAreSized { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqual(cell1.frame.size.height, 44); XCTAssertEqual(cell2.frame.size.height, 44); XCTAssertEqual(cell3.frame.size.height, 44); XCTAssertEqual(cell1.frame.size.width, 100); XCTAssertEqual(cell2.frame.size.width, 100); XCTAssertEqual(cell3.frame.size.width, 100); } - (void)test_whenItemUpdated_thatCellIsConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; self.dataSource.objects = @[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Qux"), // new value genTestObject(@3, @"Baz"), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; XCTAssertEqualObjects(cell2.label.text, @"Qux"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } @end #endif ================================================ FILE: Tests/IGListSingleSectionControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListAdapterInternal.h" #import "IGListTestCase.h" #import "IGTestCell.h" #import "IGTestSingleItemDataSource.h" #import "IGTestSingleWithoutDeselectionDelegate.h" @interface IGListSingleSectionControllerTests : IGListTestCase @end @implementation IGListSingleSectionControllerTests - (void)setUp { self.dataSource = [IGTestSingleItemDataSource new]; self.frame = CGRectMake(0, 0, 100, 1000); [super setUp]; } - (void)test_whenDisplayingCollectionView_thatSectionsHaveOneItem { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1); } - (void)test_whenDisplayingCollectionView_thatCellsAreConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); XCTAssertEqualObjects(cell3.label.text, @"Baz"); } - (void)test_whenDisplayingCollectionView_thatCellsAreSized { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestCell *cell1 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestCell *cell3 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqual(cell1.frame.size.height, 44); XCTAssertEqual(cell2.frame.size.height, 44); XCTAssertEqual(cell3.frame.size.height, 44); XCTAssertEqual(cell1.frame.size.width, 100); XCTAssertEqual(cell2.frame.size.width, 100); XCTAssertEqual(cell3.frame.size.width, 100); } - (void)test_whenItemUpdated_thatCellIsConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; self.dataSource.objects = @[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Qux"), // new value genTestObject(@3, @"Baz"), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestCell *cell2 = (IGTestCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; XCTAssertEqualObjects(cell2.label.text, @"Qux"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_whenSelected_thatDelegateReceivesEvent { [self setupWithObjects:@[ genTestObject(@1, @"a") ]]; IGListSingleSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListSingleSectionControllerDelegate)]; section.selectionDelegate = mockDelegate; [[mockDelegate expect] didSelectSectionController:section withObject:self.dataSource.objects.firstObject]; [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; [mockDelegate verify]; } - (void)test_whenDeselected_thatDelegateReceivesEvent { [self setupWithObjects:@[ genTestObject(@1, @"a") ]]; IGListSingleSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(IGListSingleSectionControllerDelegate)]; section.selectionDelegate = mockDelegate; [[mockDelegate expect] didDeselectSectionController:section withObject:self.dataSource.objects.firstObject]; [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; [mockDelegate verify]; } - (void)test_whenDeselected_withoutImplementation_thatNoOps { [self setupWithObjects:@[ genTestObject(@1, @"a") ]]; IGListSingleSectionController *section = [self.adapter sectionControllerForObject:self.dataSource.objects.firstObject]; IGTestSingleWithoutDeselectionDelegate *delegate = [IGTestSingleWithoutDeselectionDelegate new]; section.selectionDelegate = delegate; [self.adapter collectionView:self.collectionView didDeselectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertFalse(delegate.selected); [self.adapter collectionView:self.collectionView didSelectItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; XCTAssertTrue(delegate.selected); } @end ================================================ FILE: Tests/IGListSingleStoryboardItemControllerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListTestCase.h" #import "IGTestSingleStoryboardItemDataSource.h" #import "IGTestStoryboardCell.h" #import "IGTestStoryboardViewController.h" #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] #if !TARGET_OS_TV @interface IGListSingleStoryboardSectionControllerTests : XCTestCase @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) IGListAdapterUpdater *updater; @property (nonatomic, strong) IGTestSingleStoryboardItemDataSource *dataSource; @property (nonatomic, strong) IGTestStoryboardViewController *viewController; @property (nonatomic, strong) UIWindow *window; @end @implementation IGListSingleStoryboardSectionControllerTests - (void)setUp { [super setUp]; self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 1000)]; UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"IGTestStoryboard" bundle:[NSBundle bundleForClass:self.class]]; self.viewController = [storyboard instantiateViewControllerWithIdentifier:@"testVC"]; [self.window addSubview:self.viewController.view]; [self.viewController performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES]; self.collectionView = self.viewController.collectionView; self.dataSource = [[IGTestSingleStoryboardItemDataSource alloc] init]; self.updater = [[IGListAdapterUpdater alloc] init]; self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:self.viewController workingRangeSize:2]; } - (void)tearDown { [super tearDown]; self.window = nil; self.viewController = nil; self.collectionView = nil; self.adapter = nil; } - (void)setupWithObjects:(NSArray *)objects { self.dataSource.objects = objects; self.adapter.collectionView = self.viewController.collectionView; self.adapter.dataSource = self.dataSource; [self.collectionView layoutIfNeeded]; } - (void)test_whenDisplayingCollectionView_thatSectionsHaveOneItem { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; XCTAssertEqual([self.collectionView numberOfSections], 3); XCTAssertEqual([self.collectionView numberOfItemsInSection:0], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:1], 1); XCTAssertEqual([self.collectionView numberOfItemsInSection:2], 1); } - (void)test_whenDisplayingCollectionView_thatCellsAreConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestStoryboardCell *cell1 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestStoryboardCell *cell2 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestStoryboardCell *cell3 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqualObjects(cell1.label.text, @"Foo"); XCTAssertEqualObjects(cell2.label.text, @"Bar"); XCTAssertEqualObjects(cell3.label.text, @"Baz"); } - (void)test_whenDisplayingCollectionView_thatCellsAreSized { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; IGTestStoryboardCell *cell1 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]]; IGTestStoryboardCell *cell2 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; IGTestStoryboardCell *cell3 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2]]; XCTAssertEqual(cell1.frame.size.height, 44); XCTAssertEqual(cell2.frame.size.height, 44); XCTAssertEqual(cell3.frame.size.height, 44); } - (void)test_whenItemUpdated_thatCellIsConfigured { [self setupWithObjects:@[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Bar"), genTestObject(@3, @"Baz"), ]]; self.dataSource.objects = @[ genTestObject(@1, @"Foo"), genTestObject(@2, @"Qux"), // new value genTestObject(@3, @"Baz"), ]; XCTestExpectation *expectation = genExpectation; [self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) { IGTestStoryboardCell *cell2 = (IGTestStoryboardCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1]]; XCTAssertEqualObjects(cell2.label.text, @"Qux"); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:30 handler:nil]; } @end #endif ================================================ FILE: Tests/IGListTestCase.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestHelpers.h" @protocol IGListTestCaseDataSource - (NSArray *)objects; - (void)setObjects:(NSArray> *)objects; @end @interface IGListTestCase : XCTestCase // These objects are created for you in -setUp @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) IGListAdapter *adapter; @property (nonatomic, strong) UICollectionViewFlowLayout *layout; // Created in -setUp if your subclass has not already created one @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, assign) CGRect frame; // default 0,0,100,100 @property (nonatomic, strong) id updater; // default IGListAdapterUpdater // Required objects must be set before [super setUp] in your test subclass @property (nonatomic, strong) id dataSource; // Optional properties that you can set before [super setUp] @property (nonatomic, strong) UIViewController *viewController; // default nil @property (nonatomic, assign) NSInteger workingRangeSize; // default 0 // Call to configure, layout, and display the adapter and collection view - (void)setupWithObjects:(NSArray *)objects; @end ================================================ FILE: Tests/IGListTestCase.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestCase.h" @implementation IGListTestCase - (void)setUp { [super setUp]; IGAssert(self.dataSource != nil, @"Data source must be set in -setUp before testing %@", NSStringFromClass(self.class)); if (CGRectEqualToRect(self.frame, CGRectZero)) { self.frame = CGRectMake(0, 0, 100, 100); } self.window = [[UIWindow alloc] initWithFrame:self.frame]; self.layout = [UICollectionViewFlowLayout new]; self.collectionView = self.collectionView ?: [[UICollectionView alloc] initWithFrame:self.frame collectionViewLayout:self.layout]; [self.window addSubview:self.collectionView]; self.updater = self.updater ?: [IGListAdapterUpdater new]; self.adapter = [[IGListAdapter alloc] initWithUpdater:self.updater viewController:self.viewController workingRangeSize:self.workingRangeSize]; } - (void)tearDown { self.window = nil; self.collectionView = nil; self.adapter = nil; self.dataSource = nil; self.updater = nil; self.viewController = nil; self.workingRangeSize = 0; [super tearDown]; } - (void)setupWithObjects:(NSArray *)objects { self.dataSource.objects = objects; self.adapter.collectionView = self.collectionView; self.adapter.dataSource = self.dataSource; [self.collectionView layoutIfNeeded]; } @end ================================================ FILE: Tests/IGListTestHelpers.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import static inline NSIndexPath *genIndexPath(NSInteger section, NSInteger item) { return [NSIndexPath indexPathForItem:item inSection:section]; } //static inline UIViewController *loadViewController(NSString *storyboard, Class testClass, UIWin) #define genExpectation [self expectationWithDescription:NSStringFromSelector(_cmd)] #define waitExpectation [self waitForExpectationsWithTimeout:30 handler:nil] #define IGAssertEqualPoint(point, x, y, ...) \ do { \ CGPoint p = CGPointMake(x, y); \ XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ } while(0) #define IGAssertEqualSize(size, w, h, ...) \ do { \ CGSize s = CGSizeMake(w, h); \ XCTAssertEqual(CGSizeEqualToSize(size, s), YES); \ } while(0) #define IGAssertEqualFrame(frame, x, y, w, h, ...) \ do { \ CGRect expected = CGRectMake(x, y, w, h); \ XCTAssertEqual(CGRectGetMinX(expected), CGRectGetMinX(frame)); \ XCTAssertEqual(CGRectGetMinY(expected), CGRectGetMinY(frame)); \ XCTAssertEqual(CGRectGetWidth(expected), CGRectGetWidth(frame)); \ XCTAssertEqual(CGRectGetHeight(expected), CGRectGetHeight(frame)); \ } while(0) #define IGAssertEqualInsets(insets,t,l,b,r, ...) \ do { \ XCTAssertEqual(insets.top,t); \ XCTAssertEqual(insets.left,l); \ XCTAssertEqual(insets.bottom,b); \ XCTAssertEqual(insets.right,r); \ } while(0) #define IGAssertContains(collection, object) do {\ id haystack = collection; id needle = object; \ XCTAssertTrue([haystack containsObject:needle], @"%@ does not contain %@", haystack, needle); \ } while(0) ================================================ FILE: Tests/IGListTransactionTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import "IGListAdapterUpdater.h" #import "IGListBatchUpdateTransaction.h" #import "IGListDataSourceChangeTransaction.h" #import "IGListReloadTransaction.h" #import "IGListTestUICollectionViewDataSource.h" #import "IGListTransitionData.h" @interface IGListBatchUpdateTransaction (Tests) - (NSInteger)mode; - (void)setSectionData:(IGListTransitionData *)sectionData; @end @interface IGListTransactionTests : XCTestCase { IGListUpdateTransactationConfig _config; } @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) IGListTestUICollectionViewDataSource *dataSource; @property (nonatomic, strong) IGListTransitionDataApplyBlock applySectionDataBlock; @end @implementation IGListTransactionTests - (IGListCollectionViewBlock)collectionViewBlock { return ^UICollectionView *{ return self.collectionView; }; } - (IGListTransitionDataBlock)dataBlockFromObjects:(NSArray *)fromObjects toObjects:(NSArray *)toObjects { return ^IGListTransitionData *{ return [[IGListTransitionData alloc] initFromObjects:fromObjects toObjects:toObjects toSectionControllers:@[]]; }; } - (IGListBatchUpdateTransaction *)makeBatchUpdateTransaction { return [[IGListBatchUpdateTransaction alloc] initWithCollectionViewBlock:[self collectionViewBlock] updater:[IGListAdapterUpdater new] delegate:nil config:_config animated:NO sectionDataBlock:[self dataBlockFromObjects:@[] toObjects:@[@0]] applySectionDataBlock:self.applySectionDataBlock itemUpdateBlocks:@[] completionBlocks:@[]]; } - (IGListDataSourceChangeTransaction *)makeDataSourceChangeTransaction { return [[IGListDataSourceChangeTransaction alloc] initWithChangeBlock:^{} itemUpdateBlocks:@[] completionBlocks:@[]]; } - (IGListReloadTransaction *)makeReloadTransaction { return [[IGListReloadTransaction alloc] initWithCollectionViewBlock:[self collectionViewBlock] updater:[IGListAdapterUpdater new] delegate:nil reloadBlock:^{} itemUpdateBlocks:@[] completionBlocks:@[]]; } - (void)setUp { [super setUp]; self.window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 480)]; UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; self.collectionView = [[UICollectionView alloc] initWithFrame:self.window.frame collectionViewLayout:layout]; [self.window addSubview:self.collectionView]; self.dataSource = [[IGListTestUICollectionViewDataSource alloc] initWithCollectionView:self.collectionView]; __weak __typeof__(self) weakSelf = self; self.applySectionDataBlock = ^(IGListTransitionData *data) { weakSelf.dataSource.sections = data.toObjects; }; memset(&_config, 0, sizeof(IGListUpdateTransactationConfig)); } - (void)tearDown { [super tearDown]; self.collectionView = nil; self.dataSource = nil; self.window = nil; memset(&_config, 0, sizeof(IGListUpdateTransactationConfig)); } - (void)test_withBatchUpdateTransaction_thatNilCollectionViewBailsCorrectly { self.collectionView = nil; IGListBatchUpdateTransaction *batchUpdateTransaction = [self makeBatchUpdateTransaction]; [batchUpdateTransaction begin]; XCTAssertEqual(batchUpdateTransaction.state, IGListBatchUpdateStateIdle); } - (void)test_withBatchUpdateTransaction_thatCancellingTransactionMultipleTimesPerformsCorrectly { _config.allowsBackgroundDiffing = YES; IGListBatchUpdateTransaction *batchUpdateTransaction = [self makeBatchUpdateTransaction]; [batchUpdateTransaction begin]; [batchUpdateTransaction cancel]; [batchUpdateTransaction cancel]; XCTAssertEqual(batchUpdateTransaction.mode, 2); } - (void)test_withBatchUpdateTransaction_thatMismatchedCollectionViewStateIsCaught { self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[]]]; IGListBatchUpdateTransaction *batchUpdateTransaction = [self makeBatchUpdateTransaction]; @try { [batchUpdateTransaction begin]; } @catch (NSException *exception) {} } - (void)test_withBatchUpdateTransaction_thatCancellingTransactionBetweenRunLoopsIsCaptured { _config.allowsBackgroundDiffing = YES; IGListBatchUpdateTransaction *batchUpdateTransaction = [self makeBatchUpdateTransaction]; [batchUpdateTransaction begin]; [batchUpdateTransaction cancel]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; dispatch_async(dispatch_get_main_queue(), ^{ XCTAssertEqual(batchUpdateTransaction.mode, 2); // Check mode is cancelled [expectation fulfill]; }); [self waitForExpectationsWithTimeout:30 handler:nil]; } - (void)test_withDataSourceChangeTransaction_thatAllStubbedMethodsNoOpCorrectly { IGListDataSourceChangeTransaction *transaction = [self makeDataSourceChangeTransaction]; XCTAssertFalse([transaction cancel]); NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *to = [NSIndexPath indexPathForItem:0 inSection:1]; [transaction insertItemsAtIndexPaths:@[]]; [transaction deleteItemsAtIndexPaths:@[]]; [transaction moveItemFromIndexPath:from toIndexPath:to]; [transaction reloadItemFromIndexPath:from toIndexPath:to]; [transaction reloadSections:[NSIndexSet indexSet]]; } - (void)test_withReloadTransaction_thatAllStubbedMethodsNoOpCorrectly { IGListReloadTransaction *transaction = [self makeReloadTransaction]; XCTAssertFalse([transaction cancel]); NSIndexPath *from = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *to = [NSIndexPath indexPathForItem:0 inSection:1]; [transaction insertItemsAtIndexPaths:@[]]; [transaction deleteItemsAtIndexPaths:@[]]; [transaction moveItemFromIndexPath:from toIndexPath:to]; [transaction reloadItemFromIndexPath:from toIndexPath:to]; [transaction reloadSections:[NSIndexSet indexSet]]; } - (void)test_withIncorrectUpdatesState_thatInconsistencyExceptionIsCaught { _config.allowsBackgroundDiffing = NO; _config.experiments |= IGListExperimentThrowOnInconsistencyException; __weak __typeof__(self) weakSelf = self; self.applySectionDataBlock = ^(IGListTransitionData *data) { [weakSelf.collectionView deleteItemsAtIndexPaths:@[[NSIndexPath indexPathWithIndex:0]]]; }; BOOL exceptionWasHandled = NO; IGListBatchUpdateTransaction *batchUpdateTransaction = [self makeBatchUpdateTransaction]; @try { [batchUpdateTransaction begin]; } @catch (NSException *exception) { exceptionWasHandled = YES; XCTAssertTrue([exception.name isEqualToString:@"NSInternalInconsistencyException"]); } XCTAssertTrue(exceptionWasHandled); } @end ================================================ FILE: Tests/IGListUpdateCoalescerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListUpdateCoalescer.h" #import "IGListViewVisibilityTrackerInternal.h" @interface IGListUpdateCoalescerTests : XCTestCase @property (nonatomic, strong) IGListUpdateCoalescer *coalescer; @property (nonatomic, strong) XCTestExpectation *updateExpectation; @property (nonatomic, assign) NSInteger updateCount; @end @implementation IGListUpdateCoalescerTests - (void)setUp { [super setUp]; self.coalescer = [[IGListUpdateCoalescer alloc] init]; self.coalescer.delegate = self; self.updateCount = 0; } - (void)tearDown { self.coalescer = nil; self.updateExpectation = nil; [super tearDown]; } #pragma mark - IGListUpdateCoalescerDelegate - (void)performUpdateWithCoalescer:(IGListUpdateCoalescer *)coalescer { self.updateCount++; [self.updateExpectation fulfill]; } #pragma mark - Regular Dispatch - (void)test_whenQueueingUpdate_thatDelegateIsCalled { self.updateExpectation = [self expectationWithDescription:@"Update performed"]; [self.coalescer queueUpdateForView:nil]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); } - (void)test_whenQueueingMultipleUpdates_thatOnlyOneUpdateIsPerformed { self.updateExpectation = [self expectationWithDescription:@"Update performed"]; [self.coalescer queueUpdateForView:nil]; [self.coalescer queueUpdateForView:nil]; [self.coalescer queueUpdateForView:nil]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); } #pragma mark - Adaptive Coalescing Config - (void)test_whenSettingAdaptiveConfig_thatConfigIsStored { IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.1, .intervalIncrement = 0.05, .maxInterval = 0.5, .useMaxIntervalWhenViewNotVisible = NO }; self.coalescer.adaptiveCoalescingExperimentConfig = config; XCTAssertTrue(self.coalescer.adaptiveCoalescingExperimentConfig.enabled); XCTAssertEqual(self.coalescer.adaptiveCoalescingExperimentConfig.minInterval, 0.1); XCTAssertEqual(self.coalescer.adaptiveCoalescingExperimentConfig.maxInterval, 0.5); } #pragma mark - Adaptive Dispatch - (void)test_whenAdaptiveEnabled_withNoLastUpdate_thatUpdateIsPerformedImmediately { IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.01, .intervalIncrement = 0.01, .maxInterval = 0.1, .useMaxIntervalWhenViewNotVisible = NO }; self.coalescer.adaptiveCoalescingExperimentConfig = config; self.updateExpectation = [self expectationWithDescription:@"Update performed"]; [self.coalescer queueUpdateForView:nil]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); } - (void)test_whenAdaptiveEnabled_withRecentUpdate_thatUpdateIsDelayed { IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.05, .intervalIncrement = 0.01, .maxInterval = 0.2, .useMaxIntervalWhenViewNotVisible = NO }; self.coalescer.adaptiveCoalescingExperimentConfig = config; // First update - immediate self.updateExpectation = [self expectationWithDescription:@"First update"]; [self.coalescer queueUpdateForView:nil]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); // Second update - should be delayed since we're within minInterval self.updateExpectation = [self expectationWithDescription:@"Second update"]; [self.coalescer queueUpdateForView:nil]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 2); } - (void)test_whenAdaptiveEnabled_withViewNotVisible_thatMaxIntervalIsUsed { UIView *view = [[UIView alloc] init]; // View not added to window, so it's not visible // Attach a tracker and set its comparedDateOverride so it returns NotVisible (not NotVisibleEarly) IGListViewVisibilityTracker *tracker = IGListViewVisibilityTrackerAttachedOnView(view); tracker.comparedDateOverride = [tracker.dateCreated dateByAddingTimeInterval:tracker.earlyTimeInterval + 1]; XCTAssertEqual(tracker.state, IGListViewVisibilityStateNotVisible); IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.01, .intervalIncrement = 0.01, .maxInterval = 0.05, .useMaxIntervalWhenViewNotVisible = YES }; self.coalescer.adaptiveCoalescingExperimentConfig = config; self.updateExpectation = [self expectationWithDescription:@"Update performed"]; [self.coalescer queueUpdateForView:view]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); } - (void)test_whenAdaptiveEnabled_withVisibleView_thatUpdateIsPerformed { UIWindow *window = [[UIWindow alloc] init]; window.hidden = NO; UIView *view = [[UIView alloc] init]; [window addSubview:view]; IGListAdaptiveCoalescingExperimentConfig config = { .enabled = YES, .minInterval = 0.01, .intervalIncrement = 0.01, .maxInterval = 0.1, .useMaxIntervalWhenViewNotVisible = YES }; self.coalescer.adaptiveCoalescingExperimentConfig = config; self.updateExpectation = [self expectationWithDescription:@"Update performed"]; [self.coalescer queueUpdateForView:view]; [self waitForExpectationsWithTimeout:1.0 handler:nil]; XCTAssertEqual(self.updateCount, 1); } @end ================================================ FILE: Tests/IGListViewVisibilityTrackerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListViewVisibilityTrackerInternal.h" @interface IGListViewVisibilityTrackerTests : XCTestCase @property (nonatomic, strong) UIWindow *window; @property (nonatomic, strong) UIView *containerView; @property (nonatomic, strong) UIView *view; @property (nonatomic, strong) IGListViewVisibilityTracker *tracker; @end @implementation IGListViewVisibilityTrackerTests - (void)setUp { self.window = [UIWindow new]; self.window.hidden = NO; self.containerView = [UIView new]; [self.window addSubview:self.containerView]; self.view = [UIView new]; [self.containerView addSubview:self.view]; // Advance compare date so it's not early self.tracker = [[IGListViewVisibilityTracker alloc] initWithView:self.view]; self.tracker.comparedDateOverride = [self.tracker.dateCreated dateByAddingTimeInterval:self.tracker.earlyTimeInterval + 1]; } #pragma mark - Window - (void)test_whenOnWindow_thatVisible { XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateMaybeVisible); } - (void)test_whenNoWindow_thatNotVisible { [self.view removeFromSuperview]; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } #pragma mark - Hidden - (void)test_whenOnWindow_hidden_thatNotVisible { self.view.hidden = YES; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } - (void)test_whenOnWindow_parentHidden_thatNotVisible { self.containerView.hidden = YES; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } #pragma mark - Alpha - (void)test_whenOnWindow_zeroAlpha_thatNotVisible { self.view.alpha = 0; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } - (void)test_whenOnWindow_parentZeroAlpha_thatNotVisible { self.containerView.alpha = 0; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } - (void)test_whenOnWindow_windowHidden_thatNotVisible { self.window.hidden = YES; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } - (void)test_whenOnWindow_alphaVerySmall_thatNotVisible { self.view.alpha = FLT_EPSILON / 2; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisible); } - (void)test_whenOnWindow_alphaAtEpsilon_thatVisible { self.view.alpha = FLT_EPSILON; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateMaybeVisible); } #pragma mark - Early - (void)test_whenNoWindow_andEarly_thatNotVisibleEarly { [self.view removeFromSuperview]; self.tracker.earlyTimeInterval = 1.0; self.tracker.comparedDateOverride = self.tracker.dateCreated; XCTAssertEqual(self.tracker.state, IGListViewVisibilityStateNotVisibleEarly); } - (void)test_whenNoWindow_andEarly_withoutDateOverride_thatNotVisibleEarly { UIView *const newView = [UIView new]; IGListViewVisibilityTracker *const newTracker = [[IGListViewVisibilityTracker alloc] initWithView:newView]; newTracker.earlyTimeInterval = 100.0; // Large interval ensures we're still "early" // No comparedDateOverride set, so it uses [NSDate date] XCTAssertEqual(newTracker.state, IGListViewVisibilityStateNotVisibleEarly); } #pragma mark - Attached - (void)test_whenAttachingTracker_thatReturnsTheSame { UIView *const view = [UIView new]; IGListViewVisibilityTracker *const tracker1 = IGListViewVisibilityTrackerAttachedOnView(view); XCTAssertNotNil(tracker1); IGListViewVisibilityTracker *const tracker2 = IGListViewVisibilityTrackerAttachedOnView(view); XCTAssertEqual(tracker1, tracker2); } - (void)test_whenAttachingTracker_andViewIsNil_thatReturnsNil { UIView *view = [UIView new]; view = nil; XCTAssertNil(IGListViewVisibilityTrackerAttachedOnView(view)); } @end ================================================ FILE: Tests/IGListWorkingRangeHandlerTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import #import "IGListAdapterInternal.h" #import "IGListTestAdapterDataSource.h" #import "IGListTestSection.h" #import "IGListWorkingRangeHandler.h" @interface _IGTestWorkingRangeAdapterDataSource : NSObject - (instancetype)initWithObjects:(NSArray *)objects objectToControllerMap:(NSDictionary *)map; - (void)insertObject:(id)object withController:(IGListSectionController *)controller atIndex:(NSInteger)index; - (void)removeObjectAtIndex:(NSInteger)index; @end @implementation _IGTestWorkingRangeAdapterDataSource { NSArray *_objects; NSDictionary *_map; } - (instancetype)initWithObjects:(NSArray *)objects objectToControllerMap:(NSDictionary *)map { if (self = [super init]) { _objects = objects; _map = map; } return self; } - (void)insertObject:(id)object withController:(IGListSectionController *)controller atIndex:(NSInteger)index { NSMutableArray *const objects = [_objects mutableCopy]; NSMutableDictionary *const map = [_map mutableCopy]; [objects insertObject:object atIndex:index]; map[object] = controller; _objects = [objects copy]; _map = [map copy]; } - (void)removeObjectAtIndex:(NSInteger)index { NSMutableArray *const objects = [_objects mutableCopy]; NSMutableDictionary *const map = [_map mutableCopy]; [map removeObjectForKey:[objects objectAtIndex:index]]; [objects removeObjectAtIndex:index]; _objects = [objects copy]; _map = [map copy]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return _objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [_map objectForKey:object]; } @end @interface IGListWorkingRangeHandlerTests : XCTestCase @end @implementation IGListWorkingRangeHandlerTests - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeZero_thatItemEntersWorkingRange { // Arrange 1: Set up a simple collection view and adapter with a single element. IGListTestSection *controller = [[IGListTestSection alloc] init]; NSString *object = @"obj"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object] objectToControllerMap:@{object: controller}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller.workingRangeDelegate = mockWorkingRangeDelegate; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the first, and only item in the list will be displayed. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeZero_thatAdjacentItemsDoNotEnterWorkingRange { // Arrange 1: Set up a simple collection view and adapter with three elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; IGListTestSection *controller3 = [[IGListTestSection alloc] init]; NSString *object3 = @"obj3"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2, object3] objectToControllerMap:@{object1: controller1, object2: controller2, object3: controller3}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate3 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; controller3.workingRangeDelegate = mockWorkingRangeDelegate3; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the center item will be displayed. [[mockWorkingRangeDelegate1 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [[mockWorkingRangeDelegate3 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; [mockWorkingRangeDelegate3 verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeOne_thatAdjacentItemsEnterWorkingRange { // Arrange 1: Set up a simple collection view and adapter with three elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; IGListTestSection *controller3 = [[IGListTestSection alloc] init]; NSString *object3 = @"obj3"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2, object3] objectToControllerMap:@{object1: controller1, object2: controller2, object3: controller3}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate3 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; controller3.workingRangeDelegate = mockWorkingRangeDelegate3; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the center item will be displayed. [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller1]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [[mockWorkingRangeDelegate3 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller3]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; [mockWorkingRangeDelegate3 verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeOne_thatOnlyAdjacentAndVisibleItemsEnterWorkingRange { // Arrange 1: Set up a simple collection view and adapter with five elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; IGListTestSection *controller3 = [[IGListTestSection alloc] init]; NSString *object3 = @"obj3"; IGListTestSection *controller4 = [[IGListTestSection alloc] init]; NSString *object4 = @"obj4"; IGListTestSection *controller5 = [[IGListTestSection alloc] init]; NSString *object5 = @"obj5"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2, object3, object4, object5] objectToControllerMap:@{object1: controller1, object2: controller2, object3: controller3, object4: controller4, object5: controller5}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate3 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate4 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate5 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; controller3.workingRangeDelegate = mockWorkingRangeDelegate3; controller4.workingRangeDelegate = mockWorkingRangeDelegate4; controller5.workingRangeDelegate = mockWorkingRangeDelegate5; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the center item will be displayed. [[mockWorkingRangeDelegate1 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [[mockWorkingRangeDelegate3 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller3]; [[mockWorkingRangeDelegate4 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller4]; [[mockWorkingRangeDelegate5 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:2] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; [mockWorkingRangeDelegate3 verifyWithDelay:5]; [mockWorkingRangeDelegate4 verifyWithDelay:5]; [mockWorkingRangeDelegate5 verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeZero_thenHidingThatItem_thatItemLeavesWorkingRange { // Arrange 1: Set up a simple collection view and adapter with a single element. IGListTestSection *controller = [[IGListTestSection alloc] init]; NSString *object = @"obj"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object] objectToControllerMap:@{object: controller}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller.workingRangeDelegate = mockWorkingRangeDelegate; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Arrange 3: Tell the working range handler that the first, and only item in the list will be displayed. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; // Arrange 4: Wait for the item to move in-range [mockWorkingRangeDelegate verifyWithDelay:5]; // Act: Tell the working range handler that the first item is now hidden. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerDidExitWorkingRange:controller]; [adapter.workingRangeHandler didEndDisplayingItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeOne_thatNextItemEntersWorkingRange { // Arrange 1: Set up a simple collection view and adapter with two elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2] objectToControllerMap:@{object1: controller1, object2: controller2}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller2.workingRangeDelegate = mockWorkingRangeDelegate; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the first, and only item in the list will be displayed. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate verifyWithDelay:5]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeOne_thatThirdItemDoesNotEnterWorkingRange { // Arrange 1: Set up a simple collection view and adapter with three elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; IGListTestSection *controller3 = [[IGListTestSection alloc] init]; NSString *object3 = @"obj3"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2, object3] objectToControllerMap:@{object1: controller1, object2: controller2, object3: controller3}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate3 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; controller3.workingRangeDelegate = mockWorkingRangeDelegate3; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Act: Tell the working range handler that the first, and only item in the list will be displayed. [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [[mockWorkingRangeDelegate3 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; [mockWorkingRangeDelegate3 verify]; } - (void)test_whenDisplayingItemAtPath_withWorkingRangeSizeOne_thenEndDisplayingThatItem_thatNextItemLeavesWorkingRange { // Arrange 1: Set up a simple collection view and adapter with two elements. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2] objectToControllerMap:@{object1: controller1, object2: controller2}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller2.workingRangeDelegate = mockWorkingRangeDelegate; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Arrange 3: Tell the working range handler that the first, and only item in the list will be displayed. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; // Arrange 4: Wait for the item to move in-range. [mockWorkingRangeDelegate verifyWithDelay:5]; // Act: Hide the first item, and watch for the second item to leave the working range. [[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerDidExitWorkingRange:controller2]; [adapter.workingRangeHandler didEndDisplayingItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate verifyWithDelay:5]; } - (void)test_whenDisplayingItemsAtPaths_withWorkingRangeSizeOne_thatSpuriousWorkingRangeCallsAreNotMade { // Arrange 1: Set up a simple collection view and adapter with a single element. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2] objectToControllerMap:@{object1: controller1, object2: controller2}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil workingRangeSize:1]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Arrange 3: Tell the working range handler that the first item in the list will be displayed. [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller1]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; // Act: Tell the working range handler that the second item in the list will be displayed. [[mockWorkingRangeDelegate1 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate2 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; } - (void)DISABLED_test_whenDisplayingItemsAtPaths_withWorkingRangeSizeZero_thenRemovingFirstItem_thenInsertingItemAtLastPosition_thatItemEntersWorkingRange { // Arrange 1: Set up a simple collection view and adapter with a single element. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1, object2] objectToControllerMap:@{object1: controller1, object2: controller2}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Arrange 3: Tell the working range handler that the first two items in the list will be displayed. [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller1]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; // Arrange 4: Remove the object at the first index, and update the working range handler. [ds removeObjectAtIndex:0]; [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerDidExitWorkingRange:controller1]; [adapter.workingRangeHandler didEndDisplayingItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; // Act: Insert a new object at index one, and update the working range handler. [ds insertObject:object1 withController:controller1 atIndex:1]; [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller1]; [[mockWorkingRangeDelegate2 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate2 reject] listAdapter:[OCMArg any] sectionControllerDidExitWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:1] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; } - (void)DISABLED_test_whenDisplayingItemAtPath_withWorkingRangeSizeZero_thenInsertingNewItem_thatVisibleItemsRemainInWorkingRange { // Arrange 1: Set up a simple collection view and adapter with a single element. IGListTestSection *controller1 = [[IGListTestSection alloc] init]; NSString *object1 = @"obj1"; IGListTestSection *controller2 = [[IGListTestSection alloc] init]; NSString *object2 = @"obj2"; _IGTestWorkingRangeAdapterDataSource *ds = [[_IGTestWorkingRangeAdapterDataSource alloc] initWithObjects:@[object1] objectToControllerMap:@{object1: controller1}]; IGListReloadDataUpdater *updater = [[IGListReloadDataUpdater alloc] init]; IGListAdapter *adapter = [[IGListAdapter alloc] initWithUpdater:updater viewController:nil]; id collectionView = [OCMockObject niceMockForClass:[UICollectionView class]]; adapter.collectionView = collectionView; id mockWorkingRangeDelegate1 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; id mockWorkingRangeDelegate2 = [OCMockObject mockForProtocol:@protocol(IGListWorkingRangeDelegate)]; adapter.dataSource = ds; controller1.workingRangeDelegate = mockWorkingRangeDelegate1; controller2.workingRangeDelegate = mockWorkingRangeDelegate2; // Arrange 2: Force an update so we get the objects we configured through the system. [adapter performUpdatesAnimated:NO completion:nil]; // Arrange 3: Tell the working range handler that the first item in the list will be displayed. [[mockWorkingRangeDelegate1 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller1]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; // Arrange 4: Insert a second object in the first index. [ds insertObject:object2 withController:controller2 atIndex:0]; // Act: Tell the working range handler that the new item will become visible. [[mockWorkingRangeDelegate1 reject] listAdapter:[OCMArg any] sectionControllerWillEnterWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate1 reject] listAdapter:[OCMArg any] sectionControllerDidExitWorkingRange:[OCMArg any]]; [[mockWorkingRangeDelegate2 expect] listAdapter:adapter sectionControllerWillEnterWorkingRange:controller2]; [[mockWorkingRangeDelegate2 reject] listAdapter:[OCMArg any] sectionControllerDidExitWorkingRange:[OCMArg any]]; [adapter.workingRangeHandler willDisplayItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0] forListAdapter:adapter]; [mockWorkingRangeDelegate1 verifyWithDelay:5]; [mockWorkingRangeDelegate2 verifyWithDelay:5]; } @end ================================================ FILE: Tests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Tests/Objects/IGLayoutTestDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGLayoutTestSection; @interface IGLayoutTestDataSource : NSObject @property (nonatomic, copy) NSArray *sections; // call before using as the data source so cells and headers are configured - (void)configCollectionView:(UICollectionView *)collectionView; @end ================================================ FILE: Tests/Objects/IGLayoutTestDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGLayoutTestDataSource.h" #import "IGLayoutTestItem.h" #import "IGLayoutTestSection.h" static NSString * const kCellIdentifier = @"cell"; static NSString * const kHeaderIdentifier = @"header"; static NSString * const kFooterIdentifier = @"footer"; @implementation IGLayoutTestDataSource - (void)configCollectionView:(UICollectionView *)collectionView { [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:kCellIdentifier]; [collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kHeaderIdentifier]; [collectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:kFooterIdentifier]; } #pragma mark - UICollectionViewDataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return [self.sections count]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [self.sections[section].items count]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { return [collectionView dequeueReusableCellWithReuseIdentifier:kCellIdentifier forIndexPath:indexPath]; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { NSString *reuseIdentifier = [kind isEqualToString:UICollectionElementKindSectionHeader]? kHeaderIdentifier : kFooterIdentifier; return [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:reuseIdentifier forIndexPath:indexPath]; } #pragma mark - UICollectionViewDelegateFlowLayout - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return self.sections[section].insets; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return self.sections[indexPath.section].items[indexPath.item].size; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return self.sections[section].lineSpacing; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return self.sections[section].interitemSpacing; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { return CGSizeMake(self.sections[section].headerHeight, self.sections[section].headerHeight); // Only the dimension along scrolling direction is used } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { return CGSizeMake(self.sections[section].footerHeight, self.sections[section].footerHeight); // Only the dimension along scrolling direction is used } @end ================================================ FILE: Tests/Objects/IGLayoutTestItem.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #define genLayoutTestItem(s) [[IGLayoutTestItem alloc] initWithSize:s] @interface IGLayoutTestItem : NSObject @property (nonatomic, assign, readonly) CGSize size; - (instancetype)initWithSize:(CGSize)size; @end ================================================ FILE: Tests/Objects/IGLayoutTestItem.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGLayoutTestItem.h" @implementation IGLayoutTestItem - (instancetype)initWithSize:(CGSize)size { if (self = [super init]) { _size = size; } return self; } @end ================================================ FILE: Tests/Objects/IGLayoutTestSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @class IGLayoutTestItem; #define genLayoutTestSection(i) [[IGLayoutTestSection alloc] initWithItems:i] @interface IGLayoutTestSection : NSObject @property (nonatomic, assign, readonly) UIEdgeInsets insets; @property (nonatomic, assign, readonly) CGFloat lineSpacing; @property (nonatomic, assign, readonly) CGFloat interitemSpacing; @property (nonatomic, assign, readonly) CGFloat headerHeight; @property (nonatomic, assign, readonly) CGFloat footerHeight; @property (nonatomic, strong, readonly) NSArray *items; - (instancetype)initWithItems:(NSArray *)items; - (instancetype)initWithInsets:(UIEdgeInsets)insets lineSpacing:(CGFloat)lineSpacing interitemSpacing:(CGFloat)interitemSpacing headerHeight:(CGFloat)headerHeight footerHeight:(CGFloat)footerHeight items:(NSArray *)items NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; + (instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Tests/Objects/IGLayoutTestSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGLayoutTestSection.h" @implementation IGLayoutTestSection - (instancetype)initWithItems:(NSArray *)items { return [self initWithInsets:UIEdgeInsetsZero lineSpacing:0 interitemSpacing:0 headerHeight:0 footerHeight:0 items:items]; } - (instancetype)initWithInsets:(UIEdgeInsets)insets lineSpacing:(CGFloat)lineSpacing interitemSpacing:(CGFloat)interitemSpacing headerHeight:(CGFloat)headerHeight footerHeight:(CGFloat)footerHeight items:(NSArray *)items { if (self = [super init]) { _insets = insets; _lineSpacing = lineSpacing; _interitemSpacing = interitemSpacing; _headerHeight = headerHeight; _footerHeight = footerHeight; _items = [items copy]; } return self; } @end ================================================ FILE: Tests/Objects/IGListAdapterUpdateTester.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListAdapterUpdateTester : NSObject @property (nonatomic, assign) NSInteger hits; @property (nonatomic, assign) IGListAdapterUpdateType type; @property (nonatomic, assign) BOOL animated; @end ================================================ FILE: Tests/Objects/IGListAdapterUpdateTester.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListAdapterUpdateTester.h" @implementation IGListAdapterUpdateTester - (void)listAdapter:(IGListAdapter *)listAdapter didFinishUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated { self.hits++; self.type = update; self.animated = animated; } @end ================================================ FILE: Tests/Objects/IGListTestAdapterDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" @interface IGListTestAdapterDataSource : NSObject // array of numbers which is then passed to -[IGListTestSection setItems:] @property (nonatomic, copy) NSArray *objects; @property (nonatomic, strong) UIView *backgroundView; @end ================================================ FILE: Tests/Objects/IGListTestAdapterDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestAdapterDataSource.h" #import #import "IGListTestContainerSizeSection.h" #import "IGListTestSection.h" @implementation IGListTestAdapterDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { if ([object isKindOfClass:[NSNumber class]]) { if ([(NSNumber*)object isEqual: @42]) { return [IGListTestContainerSizeSection new]; } return [IGListTestSection new]; } return nil; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return self.backgroundView; } @end ================================================ FILE: Tests/Objects/IGListTestAdapterHorizontalDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestAdapterHorizontalDataSource : NSObject // array of numbers which is then passed to -[IGListTestSection setItems:] @property (nonatomic, copy) NSArray *objects; @property (nonatomic, strong) UIView *backgroundView; @end ================================================ FILE: Tests/Objects/IGListTestAdapterHorizontalDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestAdapterHorizontalDataSource.h" #import #import "IGListTestHorizontalSection.h" @implementation IGListTestAdapterHorizontalDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { IGListTestHorizontalSection *list = [[IGListTestHorizontalSection alloc] init]; return list; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return self.backgroundView; } @end ================================================ FILE: Tests/Objects/IGListTestAdapterReorderingDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListTestCase.h" @interface IGListTestAdapterReorderingDataSource : NSObject // array of numbers which is then passed to -[IGTestReorderableSection setItems:] @property (nonatomic, copy) NSArray *objects; @property (nonatomic, strong) UIView *backgroundView; @end ================================================ FILE: Tests/Objects/IGListTestAdapterReorderingDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestAdapterReorderingDataSource.h" #import #import "IGTestReorderableSection.h" @implementation IGListTestAdapterReorderingDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [IGTestReorderableSection new]; } - (nullable UIView *)emptyViewForListAdapter:(nonnull IGListAdapter *)listAdapter { return self.backgroundView; } #pragma mark - IGListAdapterMoveDelegate - (void)listAdapter:(IGListAdapter *)listAdapter moveObject:(id)object from:(NSArray *)previousObjects to:(NSArray *)objects { self.objects = objects; } @end ================================================ FILE: Tests/Objects/IGListTestAdapterStoryboardDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestAdapterStoryboardDataSource : NSObject // array of numbers which is then passed to -[IGListTestSection setItems:] @property (nonatomic, copy) NSArray *objects; @property (nonatomic, strong) UIView *backgroundView; @end ================================================ FILE: Tests/Objects/IGListTestAdapterStoryboardDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestAdapterStoryboardDataSource.h" #import #import "IGListTestStoryboardSection.h" @implementation IGListTestAdapterStoryboardDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { IGListTestStoryboardSection *list = [[IGListTestStoryboardSection alloc] init]; return list; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return self.backgroundView; } @end ================================================ FILE: Tests/Objects/IGListTestCollectionViewLayout.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN /// Layout that 1) creates all attributes regardless of size, and 2) positions them with origin (0,0) @interface IGListTestCollectionViewLayout : UICollectionViewLayout @end NS_ASSUME_NONNULL_END ================================================ FILE: Tests/Objects/IGListTestCollectionViewLayout.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestCollectionViewLayout.h" @implementation IGListTestCollectionViewLayout { NSDictionary *_attributes; } - (void)prepareLayout { UICollectionView *const collectionView = self.collectionView; // Get the UICollectionViewDelegateFlowLayout for sizes if (![collectionView.delegate conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)]) { _attributes = nil; return; } const id flowDelegate = (id)collectionView.delegate; // Create the attributes NSMutableDictionary *const attributes = [NSMutableDictionary new]; const NSInteger numberOfSections = collectionView.numberOfSections; for (NSInteger section = 0; section < numberOfSections; section++) { const NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; for (NSInteger item = 0; item < numberOfItems; item++) { NSIndexPath *const indexPath = [NSIndexPath indexPathForItem:item inSection:section]; UICollectionViewLayoutAttributes *const attribute = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; const CGSize size = [flowDelegate collectionView:collectionView layout:self sizeForItemAtIndexPath:indexPath]; attribute.frame = CGRectMake(0, 0, size.width, size.height); attributes[indexPath] = attribute; } } _attributes = [attributes copy]; } - (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect { NSMutableArray *attributes = [NSMutableArray new]; [_attributes enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath, UICollectionViewLayoutAttributes *attribute, BOOL* stop) { if (CGRectIntersectsRect(attribute.frame, rect)) { [attributes addObject:attribute]; } }]; return [attributes copy]; } - (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return _attributes[indexPath]; } @end ================================================ FILE: Tests/Objects/IGListTestContainerSizeSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestContainerSizeSection : IGListSectionController @property (nonatomic, assign) NSInteger items; @end ================================================ FILE: Tests/Objects/IGListTestContainerSizeSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestContainerSizeSection.h" @implementation IGListTestContainerSizeSection - (instancetype)init { self = [super init]; if (self) { self.inset = UIEdgeInsetsMake(1.0, 1.0, 1.0, 1.0); } return self; } - (NSArray *)cellClasses { return @[UICollectionViewCell.class]; } - (NSInteger)numberOfItems { return self.items; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return CGSizeMake(100, 10); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { return [self.collectionContext dequeueReusableCellOfClass:UICollectionViewCell.class forSectionController:self atIndex:index]; } - (void)didUpdateToObject:(id)object { if ([object isKindOfClass:[NSNumber class]]) { self.items = [object integerValue]; } } - (void)didSelectItemAtIndex:(NSInteger)index { } @end ================================================ FILE: Tests/Objects/IGListTestHorizontalSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestHorizontalSection : IGListSectionController @property (nonatomic, assign) NSInteger items; @property (nonatomic, assign) BOOL wasSelected; @end ================================================ FILE: Tests/Objects/IGListTestHorizontalSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestHorizontalSection.h" @implementation IGListTestHorizontalSection - (NSArray *)cellClasses { return @[UICollectionViewCell.class]; } - (NSInteger)numberOfItems { return self.items; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return CGSizeMake(10, 100); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { return [self.collectionContext dequeueReusableCellOfClass:UICollectionViewCell.class forSectionController:self atIndex:index]; } - (void)didUpdateToObject:(id)object { if ([object isKindOfClass:[NSNumber class]]) { self.items = [object integerValue]; } } - (void)didSelectItemAtIndex:(NSInteger)index { self.wasSelected = YES; } @end ================================================ FILE: Tests/Objects/IGListTestOffsettingLayout.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGListTestOffsettingLayout : UICollectionViewFlowLayout @end ================================================ FILE: Tests/Objects/IGListTestOffsettingLayout.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestOffsettingLayout.h" @implementation IGListTestOffsettingLayout - (void)prepareLayout { [super prepareLayout]; self.collectionView.contentOffset = CGPointMake(0, 10); } @end ================================================ FILE: Tests/Objects/IGListTestSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestSection : IGListSectionController @property (nonatomic, assign) NSInteger items; @property (nonatomic, assign) CGSize size; @property (nonatomic, assign) BOOL wasSelected; @property (nonatomic, assign) BOOL wasDeselected; @property (nonatomic, assign) BOOL wasHighlighted; @property (nonatomic, assign) BOOL wasUnhighlighted; @property (nonatomic, assign) BOOL wasDisplayed; @property (nonatomic, assign) BOOL requestedContextMenu; @end ================================================ FILE: Tests/Objects/IGListTestSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestSection.h" @interface IGListTestSection () @end @implementation IGListTestSection - (instancetype)init { if (self = [super init]) { _size = CGSizeMake(100, 10); self.displayDelegate = self; } return self; } - (NSArray *)cellClasses { return @[UICollectionViewCell.class]; } - (NSInteger)numberOfItems { return self.items; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return self.size; } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { return [self.collectionContext dequeueReusableCellOfClass:UICollectionViewCell.class forSectionController:self atIndex:index]; } - (void)didUpdateToObject:(id)object { if ([object isKindOfClass:[NSNumber class]]) { self.items = [object integerValue]; } } - (void)didSelectItemAtIndex:(NSInteger)index { self.wasSelected = YES; } - (void)didDeselectItemAtIndex:(NSInteger)index { self.wasDeselected = YES; } - (void)didHighlightItemAtIndex:(NSInteger)index { self.wasHighlighted = YES; } - (void)didUnhighlightItemAtIndex:(NSInteger)index { self.wasUnhighlighted = YES; } #if !TARGET_OS_TV - (UIContextMenuConfiguration *)contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point { self.requestedContextMenu = YES; return nil; } #endif #pragma mark - IGListDisplayDelegate - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController { _wasDisplayed = YES; } - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController {} - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index {} - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index {} @end ================================================ FILE: Tests/Objects/IGListTestStoryboardSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGListTestStoryboardSection : IGListSectionController @property (nonatomic, assign) NSInteger items; @end ================================================ FILE: Tests/Objects/IGListTestStoryboardSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestStoryboardSection.h" #import "IGTestStoryboardCell.h" @implementation IGListTestStoryboardSection - (NSInteger)numberOfItems { return self.items; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return CGSizeMake(100, 45); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { return [self.collectionContext dequeueReusableCellFromStoryboardWithIdentifier:@"IGTestStoryboardCell" forSectionController:self atIndex:index]; } - (void)didUpdateToObject:(id)object { if ([object isKindOfClass:[NSNumber class]]) { self.items = [object integerValue]; } } - (void)didSelectItemAtIndex:(NSInteger)index {} @end ================================================ FILE: Tests/Objects/IGListTestUICollectionViewDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGSectionObject : NSObject @property (nonatomic, copy) NSArray *objects; + (instancetype)sectionWithObjects:(NSArray *)objects; + (instancetype)sectionWithObjects:(NSArray *)objects identifier:(NSString *)identifier; /** @param usesIdentifierForDiffable YES if we only use the `identifier` for -isEqualToDiffableObject. NO then we compares both the `identifier` as well as `objects`. */ + (instancetype)sectionWithObjects:(NSArray *)objects identifier:(NSString *)identifier usesIdentifierForDiffable:(BOOL)usesIdentifierForDiffable; @end @interface IGListTestUICollectionViewDataSource : NSObject @property (nonatomic, copy) NSArray *sections; - (instancetype)initWithCollectionView:(UICollectionView *)collectionView; @end ================================================ FILE: Tests/Objects/IGListTestUICollectionViewDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGListTestUICollectionViewDataSource.h" #import @implementation IGSectionObject { NSString *_identifier; BOOL _usesIdentifierForDiffable; } + (instancetype)sectionWithObjects:(NSArray *)objects { return [IGSectionObject sectionWithObjects:objects identifier:[NSUUID UUID].UUIDString usesIdentifierForDiffable:NO]; } + (instancetype)sectionWithObjects:(NSArray *)objects identifier:(NSString *)identifier { return [IGSectionObject sectionWithObjects:objects identifier:identifier usesIdentifierForDiffable:NO]; } + (instancetype)sectionWithObjects:(NSArray *)objects identifier:(NSString *)identifier usesIdentifierForDiffable:(BOOL)usesIdentifierForDiffable { IGSectionObject *object = [[IGSectionObject alloc] init]; object.objects = objects; object->_identifier = [identifier copy]; object->_usesIdentifierForDiffable = usesIdentifierForDiffable; return object; } #pragma mark - IGListDiffable - (id)diffIdentifier { return _identifier; } - (BOOL)isEqualToDiffableObject:(id)object { if (object == self) { return YES; } else if ([object isKindOfClass:IGSectionObject.class]) { IGSectionObject *sectionObject = (IGSectionObject *)object; if (_usesIdentifierForDiffable) { return [_identifier isEqualToString:sectionObject->_identifier]; } else { return [self isEqual:object]; } } else { return NO; } } - (BOOL)isEqual:(id)object { if (object == self) { return YES; } else if ([object isKindOfClass:IGSectionObject.class]) { IGSectionObject *sectionObject = (IGSectionObject *)object; return ([self.objects isEqualToArray:sectionObject.objects] && [_identifier isEqualToString:sectionObject->_identifier]); } else { return NO; } } @end @implementation IGListTestUICollectionViewDataSource - (instancetype)initWithCollectionView:(UICollectionView *)collectionView { if (self = [super init]) { collectionView.dataSource = self; [collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"cell"]; } return self; } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return self.sections.count; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return [[self.sections[section] objects] count]; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cell" forIndexPath:indexPath]; return cell; } @end ================================================ FILE: Tests/Objects/IGTestBindingSingleItemDataSource.h ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * 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 "IGTestObject.h" #import "IGListTestCase.h" NS_ASSUME_NONNULL_BEGIN @interface IGTestBindingSingleItemDataSource : NSObject @property (nonatomic, strong) NSArray *objects; @end NS_ASSUME_NONNULL_END ================================================ FILE: Tests/Objects/IGTestBindingSingleItemDataSource.m ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * 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 "IGTestBindingSingleItemDataSource.h" #import #import "IGTestCell.h" @interface IGTestBindingSingleSectionController : IGListBindingSingleSectionController @end @implementation IGTestBindingSingleSectionController - (Class)cellClass { return IGTestCell.class; } - (void)configureCell:(IGTestCell *)cell withViewModel:(IGTestObject *)viewModel { cell.label.text = [viewModel.value description]; } - (CGSize)sizeForViewModel:(IGTestObject *)viewModel { return CGSizeMake([self.collectionContext containerSize].width, 44); } - (void)didSelectItemWithCell:(IGTestCell *)cell { cell.label.text = @"did-select"; } - (void)didDeselectItemWithCell:(IGTestCell *)cell { cell.label.text = @"did-deselect"; } - (void)didHighlightItemWithCell:(IGTestCell *)cell { cell.label.text = @"did-highlight"; } - (void)didUnhighlightItemWithCell:(IGTestCell *)cell { cell.label.text = @"did-unhighlight"; } @end @implementation IGTestBindingSingleItemDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [IGTestBindingSingleSectionController new]; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestBindingWithoutDeselectionDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestBindingWithoutDeselectionDelegate : NSObject @property (nonatomic, assign) BOOL selected; @end ================================================ FILE: Tests/Objects/IGTestBindingWithoutDeselectionDelegate.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestBindingWithoutDeselectionDelegate.h" @implementation IGTestBindingWithoutDeselectionDelegate - (void)sectionController:(IGListBindingSectionController *)sectionController didSelectItemAtIndex:(NSInteger)index viewModel:(id)viewModel { self.selected = YES; } - (void)sectionController:(IGListBindingSectionController *)sectionController didDeselectItemAtIndex:(NSInteger)index viewModel:(id)viewModel; { } - (void)sectionController:(nonnull IGListBindingSectionController *)sectionController didHighlightItemAtIndex:(NSInteger)index viewModel:(nonnull id)viewModel { } - (void)sectionController:(nonnull IGListBindingSectionController *)sectionController didUnhighlightItemAtIndex:(NSInteger)index viewModel:(nonnull id)viewModel { } #if !TARGET_OS_TV - (UIContextMenuConfiguration * _Nullable)sectionController:(nonnull IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(nonnull id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { return nil; } #endif @end ================================================ FILE: Tests/Objects/IGTestCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestCell : UICollectionViewCell @property (nonatomic, weak) id delegate; @property (nonatomic, strong) IBOutlet UILabel *label; @end ================================================ FILE: Tests/Objects/IGTestCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestCell.h" #import "IGTestObject.h" @implementation IGTestCell - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _label = [[UILabel alloc] init]; [self.contentView addSubview:_label]; } return self; } - (void)bindViewModel:(id)viewModel { IGTestObject *object = viewModel; self.label.text = [object.value description]; } @end ================================================ FILE: Tests/Objects/IGTestDelegateController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGTestCell.h" @class IGTestObject; @interface IGTestDelegateController : IGListSectionController @property (nonatomic, strong) IGTestObject *item; @property (nonatomic, assign) CGFloat height; @property (nonatomic, copy) void (^itemUpdateBlock)(void); @property (nonatomic, copy) void (^cellConfigureBlock)(IGTestDelegateController *); @property (nonatomic, assign, readonly) NSInteger updateCount; @property (nonatomic, assign) NSInteger willDisplayCount; @property (nonatomic, assign) NSInteger didEndDisplayCount; @property (nonatomic, strong) NSCountedSet *willDisplayCellIndexes; @property (nonatomic, strong) NSCountedSet *didEndDisplayCellIndexes; @property (nonatomic, assign) CGPoint initialAttributesOffset; @property (nonatomic, assign) CGPoint finalAttributesOffset; @property (nonatomic, strong) IGTestCell *overrideCell; @end ================================================ FILE: Tests/Objects/IGTestDelegateController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestDelegateController.h" #import "IGTestCell.h" #import "IGTestObject.h" @implementation IGTestDelegateController - (instancetype)init { if (self = [super init]) { _willDisplayCellIndexes = [NSCountedSet new]; _didEndDisplayCellIndexes = [NSCountedSet new]; _height = 10.0; self.workingRangeDelegate = self; } return self; } - (NSInteger)numberOfItems { if ([self.item.value isKindOfClass:[NSNumber class]]) { return [self.item.value integerValue]; } return 1; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return CGSizeMake(self.collectionContext.containerSize.width, self.height); } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { IGTestCell *cell = _overrideCell ?: [self.collectionContext dequeueReusableCellOfClass:IGTestCell.class forSectionController:self atIndex:index]; [[cell label] setText:[NSString stringWithFormat:@"%@", self.item.value]]; [cell setDelegate:self]; if (self.cellConfigureBlock) { self.cellConfigureBlock(self); } return cell; } - (void)didUpdateToObject:(id)object { _updateCount++; _item = object; if (self.itemUpdateBlock) { self.itemUpdateBlock(); } } - (id)displayDelegate { return self; } - (void)didSelectItemAtIndex:(NSInteger)index {} #pragma mark - IGListDisplayDelegate - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController { self.willDisplayCount++; } - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController { self.didEndDisplayCount++; } - (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index { [self.willDisplayCellIndexes addObject:@(index)]; } - (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController cell:(UICollectionViewCell *)cell atIndex:(NSInteger)index { [self.didEndDisplayCellIndexes addObject:@(index)]; } - (void)listAdapter:(IGListAdapter *)listAdapter didScrollSectionController:(IGListSectionController *)sectionController {} #pragma mark - IGListWorkingRangeDelegate - (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController *)sectionController { __unused UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; __unused UIView *supplementaryView = [self.collectionContext viewForSupplementaryElementOfKind:UICollectionElementKindSectionHeader atIndex:0 sectionController:self]; } - (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController *)sectionController {} #pragma mark - IGListTransitionDelegate - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { attributes.center = CGPointMake(attributes.center.x + _initialAttributesOffset.x, attributes.center.y + _initialAttributesOffset.y); return attributes; } - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index { attributes.center = CGPointMake(attributes.center.x + _finalAttributesOffset.x, attributes.center.y + _finalAttributesOffset.y); return attributes; } @end ================================================ FILE: Tests/Objects/IGTestDelegateDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" #import "IGTestCell.h" @class IGTestDelegateController; @class IGTestObject; extern NSObject *const kIGTestDelegateDataSourceSkipObject; extern NSObject *const kIGTestDelegateDataSourceNoSectionControllerSubclass; @interface IGTestDelegateDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @property (nonatomic, copy) void (^cellConfigureBlock)(IGTestDelegateController *); @property (nonatomic, strong) IGTestCell *overrideCell; @end ================================================ FILE: Tests/Objects/IGTestDelegateDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestDelegateDataSource.h" #import #import "IGTestDelegateController.h" #import "IGTestObject.h" NSObject *const kIGTestDelegateDataSourceSkipObject = @"kIGTestDelegateDataSourceSkipObject"; NSObject *const kIGTestDelegateDataSourceNoSectionControllerSubclass = @"kIGTestDelegateDataSourceNoSectionControllerSubclass"; @implementation IGTestDelegateDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { if ([object isEqual:kIGTestDelegateDataSourceSkipObject]) { return nil; } else if ([object isEqual:kIGTestDelegateDataSourceNoSectionControllerSubclass]) { return [IGListSectionController new]; } IGTestDelegateController *sectionController = [[IGTestDelegateController alloc] init]; sectionController.cellConfigureBlock = self.cellConfigureBlock; sectionController.overrideCell = self.overrideCell; return sectionController; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestDiffingDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" @class IGTestDiffingObject; @interface IGTestDiffingDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestDiffingDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestDiffingDataSource.h" #import "IGTestDiffingObject.h" #import "IGTestDiffingSectionController.h" @implementation IGTestDiffingDataSource #pragma mark - IGListAdapterDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [IGTestDiffingSectionController new]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestDiffingObject.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestDiffingObject : NSObject - (instancetype)initWithKey:(id)key objects:(NSArray *)objects; @property (nonatomic, strong, readonly) id key; @property (nonatomic, strong, readonly) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestDiffingObject.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestDiffingObject.h" @implementation IGTestDiffingObject - (instancetype)initWithKey:(id)key objects:(NSArray *)objects { if (self = [super init]) { _key = key; _objects = objects; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p; key: %@; objects: %@>", NSStringFromClass(self.class), self, self.key, self.objects]; } #pragma mark - IGListDiffable - (id)diffIdentifier { return self.key; } - (BOOL)isEqualToDiffableObject:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGTestDiffingObject class]]) { /* A simple equality test that only looks at the number of objects for the key. It does not currently test the equality of each of the objects. */ IGTestDiffingObject *testDiffingObject = (IGTestDiffingObject *)object; return self.objects.count == testDiffingObject.objects.count; } return NO; } @end ================================================ FILE: Tests/Objects/IGTestDiffingSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGTestDiffingSectionController : IGListBindingSectionController @property (nonatomic, strong) id selectedViewModel; @property (nonatomic, strong) id deselectedViewModel; @property (nonatomic, strong) id highlightedViewModel; @property (nonatomic, strong) id unhighlightedViewModel; @property (nonatomic, strong) id contextMenuViewModel; @end ================================================ FILE: Tests/Objects/IGTestDiffingSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestDiffingSectionController.h" #import "IGTestCell.h" #import "IGTestDiffingObject.h" #import "IGTestNumberBindableCell.h" #import "IGTestObject.h" #import "IGTestStringBindableCell.h" @implementation IGTestDiffingSectionController - (instancetype)init { if (self = [super init]) { self.dataSource = self; self.selectionDelegate = self; } return self; } #pragma mark - IGListBindingSectionControllerDataSource - (NSArray> *)sectionController:(IGListBindingSectionController *)sectionController viewModelsForObject:(id)object { return [(IGTestDiffingObject *)object objects]; } - (UICollectionViewCell *)sectionController:(IGListBindingSectionController *)sectionController cellForViewModel:(id)viewModel atIndex:(NSInteger)index { Class cellClass; if ([viewModel isKindOfClass:[NSString class]]) { cellClass = [IGTestStringBindableCell class]; } else if ([viewModel isKindOfClass:[NSNumber class]]) { cellClass = [IGTestNumberBindableCell class]; } else { cellClass = [IGTestCell class]; } id cell = [self.collectionContext dequeueReusableCellOfClass:cellClass forSectionController:self atIndex:index]; return cell; } - (CGSize)sectionController:(IGListBindingSectionController *)sectionController sizeForViewModel:(id)viewModel atIndex:(NSInteger)index { const BOOL isString = [viewModel isKindOfClass:[NSString class]]; return CGSizeMake([self.collectionContext containerSize].width, isString ? 55 : 30); } #pragma mark - IGListBindingSectionControllerSelectionDelegate - (void)sectionController:(IGListBindingSectionController *)sectionController didSelectItemAtIndex:(NSInteger)index viewModel:(id)viewModel { self.selectedViewModel = viewModel; } - (void)sectionController:(IGListBindingSectionController *)sectionController didDeselectItemAtIndex:(NSInteger)index viewModel:(id)viewModel { self.deselectedViewModel = viewModel; } - (void)sectionController:(IGListBindingSectionController *)sectionController didHighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel { self.highlightedViewModel = viewModel; } - (void)sectionController:(IGListBindingSectionController *)sectionController didUnhighlightItemAtIndex:(NSInteger)index viewModel:(id)viewModel { self.unhighlightedViewModel = viewModel; } #if !TARGET_OS_TV - (UIContextMenuConfiguration * _Nullable)sectionController:(IGListBindingSectionController *)sectionController contextMenuConfigurationForItemAtIndex:(NSInteger)index point:(CGPoint)point viewModel:(id)viewModel API_AVAILABLE(ios(13.0)) API_UNAVAILABLE(tvos) { self.contextMenuViewModel = viewModel; return nil; } #endif @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" @class IGTestInvalidateLayoutObject; @interface IGTestInvalidateLayoutDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestInvalidateLayoutDataSource.h" #import "IGTestInvalidateLayoutObject.h" #import "IGTestInvalidateLayoutSectionController.h" @implementation IGTestInvalidateLayoutDataSource #pragma mark - IGListAdapterDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { return [IGTestInvalidateLayoutSectionController new]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutObject.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGLayoutTestItem.h" #import "IGTestObject.h" #define genInvalidateLayoutObject(key, size) [[IGTestObject alloc] initWithKey:key value:[[IGLayoutTestItem alloc] initWithSize:size]] @interface IGTestInvalidateLayoutObject : NSObject - (instancetype)initWithKey:(id)key objects:(NSArray *)objects; @property (nonatomic, strong, readonly) id key; @property (nonatomic, strong, readonly) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutObject.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestInvalidateLayoutObject.h" @implementation IGTestInvalidateLayoutObject - (instancetype)initWithKey:(id)key objects:(NSArray *)objects { if (self = [super init]) { _key = key; _objects = objects; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p; key: %@; objects: %@>", NSStringFromClass(self.class), self, self.key, self.objects]; } #pragma mark - IGListDiffable - (id)diffIdentifier { return self.key; } - (BOOL)isEqualToDiffableObject:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGTestInvalidateLayoutObject class]]) { return YES; } return NO; } @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutSectionController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGTestInvalidateLayoutSectionController : IGListBindingSectionController @end ================================================ FILE: Tests/Objects/IGTestInvalidateLayoutSectionController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestInvalidateLayoutSectionController.h" #import "IGLayoutTestItem.h" #import "IGTestCell.h" #import "IGTestInvalidateLayoutObject.h" #import "IGTestObject.h" @implementation IGTestInvalidateLayoutSectionController - (instancetype)init { if (self = [super init]) { self.dataSource = self; } return self; } #pragma mark - IGListBindingSectionControllerDataSource - (NSArray> *)sectionController:(IGListBindingSectionController *)sectionController viewModelsForObject:(id)object { return [(IGTestInvalidateLayoutObject *)object objects]; } - (UICollectionViewCell *)sectionController:(IGListBindingSectionController *)sectionController cellForViewModel:(id)viewModel atIndex:(NSInteger)index { IGTestCell *cell = [self.collectionContext dequeueReusableCellOfClass:[IGTestCell class] forSectionController:self atIndex:index]; return cell; } - (CGSize)sectionController:(IGListBindingSectionController *)sectionController sizeForViewModel:(id)viewModel atIndex:(NSInteger)index { return [(IGLayoutTestItem *)[(IGTestObject *)viewModel value] size]; } @end ================================================ FILE: Tests/Objects/IGTestNibSupplementaryView.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGTestNibSupplementaryView : UICollectionViewCell @property (nonatomic, weak) id delegate; @property (nonatomic, strong) IBOutlet UILabel *label; @end ================================================ FILE: Tests/Objects/IGTestNibSupplementaryView.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestNibSupplementaryView.h" @implementation IGTestNibSupplementaryView @end ================================================ FILE: Tests/Objects/IGTestNumberBindableCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestNumberBindableCell : UICollectionViewCell @property (nonatomic, strong, readonly) UITextField *textField; @end ================================================ FILE: Tests/Objects/IGTestNumberBindableCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestNumberBindableCell.h" @implementation IGTestNumberBindableCell - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _textField = [UITextField new]; } return self; } #pragma mark - IGListBindable - (void)bindViewModel:(id)viewModel { self.textField.text = [viewModel description]; } @end ================================================ FILE: Tests/Objects/IGTestObject.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #define genTestObject(k, v) [[IGTestObject alloc] initWithKey:k value:v] @interface IGTestObject : NSObject - (instancetype)initWithKey:(id )key value:(id)value; @property (nonatomic, strong, readonly) id key; @property (nonatomic, strong) id value; @end ================================================ FILE: Tests/Objects/IGTestObject.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestObject.h" @implementation IGTestObject - (instancetype)initWithKey:(id)key value:(id)value { if (self = [super init]) { _key = [key copy]; _value = value; } return self; } - (instancetype)copyWithZone:(NSZone *)zone { return [[IGTestObject alloc] initWithKey:self.key value:self.value]; } #pragma mark - IGListDiffable - (id)diffIdentifier { return self.key; } - (BOOL)isEqualToDiffableObject:(id)object { if (object == self) { return YES; } if ([object isKindOfClass:[IGTestObject class]]) { id k1 = self.key; id k2 = [object key]; id v1 = self.value; id v2 = [(IGTestObject *)object value]; return (v1 == v2 || [v1 isEqual:v2]) && (k1 == k2 || [k1 isEqual:k2]); } return NO; } @end ================================================ FILE: Tests/Objects/IGTestReorderableSection.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import @interface IGTestReorderableSectionObject : NSObject @property (nonatomic, copy) NSArray *objects; + (instancetype)sectionWithObjects:(NSArray *)objects; @end @interface IGTestReorderableSection : IGListSectionController @property (nonatomic, strong) IGTestReorderableSectionObject *sectionObject; @property (nonatomic, assign) CGSize size; @property (nonatomic, assign) BOOL isReorderable; - (instancetype)initWithSectionObject:(IGTestReorderableSectionObject *)sectionObject; @end ================================================ FILE: Tests/Objects/IGTestReorderableSection.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestReorderableSection.h" @implementation IGTestReorderableSectionObject + (instancetype)sectionWithObjects:(NSArray *)objects { IGTestReorderableSectionObject *object = [IGTestReorderableSectionObject new]; object.objects = objects; return object; } #pragma mark - IGListDiffable - (id)diffIdentifier { // this is for test purposes only. please dont do this. return [NSString stringWithFormat:@"%lu", (unsigned long)self.hash]; } - (BOOL)isEqualToDiffableObject:(id)object { if (object == self) { return YES; } else if ([object isKindOfClass:IGTestReorderableSectionObject.class]) { return (self.objects && [self.objects isEqualToArray:[object objects]]) || (!self.objects && ![object objects]); } else { return NO; } } @end @implementation IGTestReorderableSection - (instancetype)initWithSectionObject:(IGTestReorderableSectionObject *)sectionObject { if (self = [super init]) { _sectionObject = sectionObject; _size = CGSizeMake(100, 10); } return self; } - (NSArray *)cellClasses { return @[UICollectionViewCell.class]; } - (NSInteger)numberOfItems { return [self.sectionObject.objects count]; } - (CGSize)sizeForItemAtIndex:(NSInteger)index { return self.size; } - (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { return [self.collectionContext dequeueReusableCellOfClass:UICollectionViewCell.class forSectionController:self atIndex:index]; } - (void)didUpdateToObject:(id)object { if ([object isKindOfClass:[IGTestReorderableSection class]]) { self.sectionObject = object; } } - (BOOL)canMoveItemAtIndex:(NSInteger)index { return self.isReorderable; } - (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex { NSArray *originalObjects = self.sectionObject.objects; NSMutableArray *updatedObjects = [originalObjects mutableCopy]; id object = originalObjects[sourceIndex]; [updatedObjects removeObjectAtIndex:sourceIndex]; [updatedObjects insertObject:object atIndex:destinationIndex]; self.sectionObject.objects = [updatedObjects copy]; } @end ================================================ FILE: Tests/Objects/IGTestSingleItemDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" #import "IGTestObject.h" @interface IGTestSingleItemDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestSingleItemDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestSingleItemDataSource.h" #import #import "IGTestCell.h" @implementation IGTestSingleItemDataSource - (NSArray *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { void (^configureBlock)(id, __kindof UICollectionViewCell *) = ^(IGTestObject *item, IGTestCell *cell) { cell.label.text = [item.value description]; }; CGSize (^sizeBlock)(id, id) = ^CGSize(IGTestObject *item, id collectionContext) { return CGSizeMake([collectionContext containerSize].width, 44); }; return [[IGListSingleSectionController alloc] initWithCellClass:IGTestCell.class configureBlock:configureBlock sizeBlock:sizeBlock]; } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestSingleNibItemDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGListTestCase.h" #import "IGTestObject.h" @interface IGTestSingleNibItemDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestSingleNibItemDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestSingleNibItemDataSource.h" #import #import "IGTestCell.h" @implementation IGTestSingleNibItemDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { void (^configureBlock)(id, __kindof UICollectionViewCell *) = ^(IGTestObject *item, IGTestCell *cell) { cell.label.text = [item.value description]; }; CGSize (^sizeBlock)(id, id) = ^CGSize(IGTestObject *item, id collectionContext) { return CGSizeMake([collectionContext containerSize].width, 44); }; return [[IGListSingleSectionController alloc] initWithNibName:@"IGTestNibCell" bundle:[NSBundle bundleForClass:self.class] configureBlock:configureBlock sizeBlock:sizeBlock]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestSingleStoryboardItemDataSource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGTestObject.h" @interface IGTestSingleStoryboardItemDataSource : NSObject @property (nonatomic, copy) NSArray *objects; @end ================================================ FILE: Tests/Objects/IGTestSingleStoryboardItemDataSource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestSingleStoryboardItemDataSource.h" #import #import "IGTestStoryboardCell.h" @implementation IGTestSingleStoryboardItemDataSource - (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter { return self.objects; } - (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object { void (^configureBlock)(id, __kindof UICollectionViewCell *) = ^(IGTestObject *item, IGTestStoryboardCell *cell) { cell.label.text = [item.value description]; }; CGSize (^sizeBlock)(id, id) = ^CGSize(IGTestObject *item, id collectionContext) { return CGSizeMake([collectionContext containerSize].width, 44); }; return [[IGListSingleSectionController alloc] initWithStoryboardCellIdentifier:@"IGTestStoryboardCell" configureBlock:configureBlock sizeBlock:sizeBlock]; } - (UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { return nil; } @end ================================================ FILE: Tests/Objects/IGTestSingleWithoutDeselectionDelegate.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestSingleWithoutDeselectionDelegate : NSObject @property (nonatomic, assign) BOOL selected; @end ================================================ FILE: Tests/Objects/IGTestSingleWithoutDeselectionDelegate.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestSingleWithoutDeselectionDelegate.h" @implementation IGTestSingleWithoutDeselectionDelegate - (void)didSelectSectionController:(IGListSingleSectionController *)sectionController withObject:(id)object { self.selected = YES; } @end ================================================ FILE: Tests/Objects/IGTestStoryboardCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGTestStoryboardCell : UICollectionViewCell @property (nonatomic, weak) id delegate; @property (weak, nonatomic) IBOutlet UILabel *label; @end ================================================ FILE: Tests/Objects/IGTestStoryboardCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestStoryboardCell.h" @implementation IGTestStoryboardCell @end ================================================ FILE: Tests/Objects/IGTestStoryboardSupplementarySource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestStoryboardSupplementarySource : NSObject @property (nonatomic, copy, readwrite) NSArray *supportedElementKinds; @property (nonatomic, weak) id collectionContext; @property (nonatomic, weak) IGListSectionController *sectionController; @end ================================================ FILE: Tests/Objects/IGTestStoryboardSupplementarySource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestStoryboardSupplementarySource.h" #import "IGTestStoryboardSupplementaryView.h" @implementation IGTestStoryboardSupplementarySource - (UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index { IGTestStoryboardSupplementaryView *view = [self.collectionContext dequeueReusableSupplementaryViewFromStoryboardOfKind:elementKind withIdentifier:@"IGTestStoryboardSupplementaryView" forSectionController:self.sectionController atIndex:index]; view.label.text = @"Header"; return view; } - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index { return CGSizeMake([self.collectionContext containerSize].width, 45); } @end ================================================ FILE: Tests/Objects/IGTestStoryboardSupplementaryView.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import @interface IGTestStoryboardSupplementaryView : UICollectionReusableView @property (weak, nonatomic) IBOutlet UILabel *label; @end ================================================ FILE: Tests/Objects/IGTestStoryboardSupplementaryView.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestStoryboardSupplementaryView.h" @implementation IGTestStoryboardSupplementaryView @end ================================================ FILE: Tests/Objects/IGTestStoryboardViewController.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import "IGTestObject.h" @interface IGTestStoryboardViewController : UIViewController @property (weak, nonatomic) IBOutlet UICollectionView *collectionView; @end ================================================ FILE: Tests/Objects/IGTestStoryboardViewController.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestStoryboardViewController.h" #import "IGTestStoryboardCell.h" @interface IGTestStoryboardViewController () @end @implementation IGTestStoryboardViewController @end ================================================ FILE: Tests/Objects/IGTestStringBindableCell.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestStringBindableCell : UICollectionViewCell @property (nonatomic, strong, readonly) UILabel *label; @end ================================================ FILE: Tests/Objects/IGTestStringBindableCell.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestStringBindableCell.h" @implementation IGTestStringBindableCell - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { _label = [UILabel new]; } return self; } #pragma mark - IGListBindable - (void)bindViewModel:(id)viewModel { self.label.text = viewModel; } @end ================================================ FILE: Tests/Objects/IGTestSupplementarySource.h ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import @interface IGTestSupplementarySource : NSObject @property (nonatomic, assign) BOOL dequeueFromNib; @property (nonatomic, assign) CGSize size; @property (nonatomic, copy, readwrite) NSArray *supportedElementKinds; @property (nonatomic, weak) id collectionContext; @property (nonatomic, weak) IGListSectionController *sectionController; @end ================================================ FILE: Tests/Objects/IGTestSupplementarySource.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "IGTestSupplementarySource.h" #import "IGTestNibSupplementaryView.h" @implementation IGTestSupplementarySource - (instancetype)init { if (self = [super init]) { _size = CGSizeMake(100, 10); } return self; } #pragma mark - IGListSupplementaryViewSource - (UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind atIndex:(NSInteger)index { if (self.dequeueFromNib) { IGTestNibSupplementaryView *view = [self.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind forSectionController:self.sectionController nibName:@"IGTestNibSupplementaryView" bundle:[NSBundle bundleForClass:self.class] atIndex:index]; view.label.text = @"Foo bar baz"; return view; } else { return [self.collectionContext dequeueReusableSupplementaryViewOfKind:elementKind forSectionController:self.sectionController class:[UICollectionReusableView class] atIndex:index]; } } - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndex:(NSInteger)index { return self.size; } @end ================================================ FILE: Tests/UIViewControllerIGListAdapterTests.m ================================================ /* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import #import #import #import "IGListAdapter+UICollectionView.h" #import "IGListAssert.h" #import "IGListTestAdapterDataSource.h" #import "UIViewController+IGListAdapter.h" @interface UIViewControllerIGListAdapterTests : XCTestCase @end @implementation UIViewControllerIGListAdapterTests - (void)test_whenNoAdapter_thatReturnsEmpty { UIViewController *const viewController = [UIViewController new]; NSArray *const adapters = [viewController associatedListAdapters]; XCTAssertEqual(adapters.count, 0); } - (void)test_whenOneAdapter_thatReturnsOne { UIViewController *const viewController = [UIViewController new]; IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; NSArray *const adapters = [viewController associatedListAdapters]; XCTAssertEqual(adapters.count, 1); XCTAssertEqual(adapters.firstObject, adapter); } - (void)test_whenTwoAdapters_thatReturnsTwo { UIViewController *const viewController = [UIViewController new]; __unused IGListAdapter *const adapter1 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; __unused IGListAdapter *const adapter2 = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; NSArray *const adapters = [viewController associatedListAdapters]; XCTAssertEqual(adapters.count, 2); } - (void)test_whenOneAdapters_andDealloc_thatReturnsEmpty { UIViewController *const viewController = [UIViewController new]; @autoreleasepool { __unused IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; // let adapter get deallocated } NSArray *const adapters = [viewController associatedListAdapters]; XCTAssertEqual(adapters.count, 0); } - (void)test_whenCalledMultipleTimes_thatReturnsSameAdapters { UIViewController *const viewController = [UIViewController new]; IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; NSArray *const adapters1 = [viewController associatedListAdapters]; NSArray *const adapters2 = [viewController associatedListAdapters]; XCTAssertEqual(adapters1.count, 1); XCTAssertEqual(adapters2.count, 1); XCTAssertEqual(adapters1.firstObject, adapter); XCTAssertEqual(adapters2.firstObject, adapter); } #pragma mark - Preferred Focus - (void)test_whenCollectionViewDelegateImplementsPreferredFocus_thatDelegatesToIt { UIViewController *const viewController = [UIViewController new]; UICollectionView *const collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; IGListTestAdapterDataSource *const dataSource = [IGListTestAdapterDataSource new]; dataSource.objects = @[@0, @1, @2]; IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; [collectionView reloadData]; NSIndexPath *const expectedIndexPath = [NSIndexPath indexPathForItem:1 inSection:2]; id mockDelegate = [OCMockObject mockForProtocol:@protocol(UICollectionViewDelegate)]; [[[mockDelegate stub] andReturnValue:OCMOCK_VALUE(expectedIndexPath)] indexPathForPreferredFocusedViewInCollectionView:collectionView]; [[[mockDelegate stub] andReturnValue:@YES] respondsToSelector:@selector(indexPathForPreferredFocusedViewInCollectionView:)]; adapter.collectionViewDelegate = mockDelegate; NSIndexPath *const result = [adapter indexPathForPreferredFocusedViewInCollectionView:collectionView]; XCTAssertEqualObjects(result, expectedIndexPath); } - (void)test_whenExperimentEnabled_thatReturnsFirstVisibleIndexPath { UIWindow *const window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; UIViewController *const viewController = [UIViewController new]; UICollectionView *const collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; [window addSubview:collectionView]; IGListTestAdapterDataSource *const dataSource = [IGListTestAdapterDataSource new]; dataSource.objects = @[@0, @1, @2]; IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; adapter.experiments = IGListExperimentFixPreferredFocusedView; [collectionView reloadData]; [collectionView layoutIfNeeded]; NSIndexPath *const result = [adapter indexPathForPreferredFocusedViewInCollectionView:collectionView]; XCTAssertNotNil(result); } - (void)test_whenNoDelegate_andNoExperiment_thatReturnsNil { UIViewController *const viewController = [UIViewController new]; UICollectionView *const collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) collectionViewLayout:[UICollectionViewFlowLayout new]]; IGListTestAdapterDataSource *const dataSource = [IGListTestAdapterDataSource new]; dataSource.objects = @[@0, @1, @2]; IGListAdapter *const adapter = [[IGListAdapter alloc] initWithUpdater:[IGListAdapterUpdater new] viewController:viewController]; adapter.collectionView = collectionView; adapter.dataSource = dataSource; adapter.collectionViewDelegate = nil; adapter.experiments = IGListExperimentNone; [collectionView reloadData]; NSIndexPath *const result = [adapter indexPathForPreferredFocusedViewInCollectionView:collectionView]; XCTAssertNil(result); } @end ================================================ FILE: docs/Categories/UIViewController(IGListAdapter).html ================================================ UIViewController(IGListAdapter) Category Reference

UIViewController(IGListAdapter)

@interface UIViewController (IGListAdapter)

/// Adapters that have this VC as their `viewController`
- (NSArray<IGListAdapter *> *)associatedListAdapters;

@end

Undocumented

================================================ FILE: docs/Categories.html ================================================ Categories Reference
================================================ FILE: docs/Classes/IGListAdapter.html ================================================ IGListAdapter Class Reference

IGListAdapter

Objective-C


@interface IGListAdapter : NSObject

Swift

@MainActor class ListAdapter : NSObject

IGListAdapter objects provide an abstraction for feeds of objects in a UICollectionView by breaking each object into individual sections, called “section controllers”. These controllers (objects subclassing to IGListSectionController) act as a data source and delegate for each section.

Feed implementations must act as the data source for an IGListAdapter in order to drive the objects and section controllers in a collection view.

  • The view controller that houses the adapter.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) UIViewController *viewController;

    Swift

    weak var viewController: UIViewController? { get set }
  • The collection view used with the adapter.

    Note

    Setting this property will automatically set isPrefetchingEnabled to NO for performance reasons.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) UICollectionView *collectionView;

    Swift

    weak var collectionView: UICollectionView? { get set }
  • The object that acts as the data source for the adapter.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListAdapterDataSource> dataSource;

    Swift

    weak var dataSource: (any ListAdapterDataSource)? { get set }
  • The object that receives top-level events for section controllers.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListAdapterDelegate> delegate;

    Swift

    weak var delegate: (any ListAdapterDelegate)? { get set }
  • The object that receives UICollectionViewDelegate events.

    Note

    This object will not receive UIScrollViewDelegate events. Instead use scrollViewDelegate.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<UICollectionViewDelegate> collectionViewDelegate;

    Swift

    weak var collectionViewDelegate: (any UICollectionViewDelegate)? { get set }
  • The object that receives UIScrollViewDelegate events.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<UIScrollViewDelegate> scrollViewDelegate;

    Swift

    weak var scrollViewDelegate: (any UIScrollViewDelegate)? { get set }
  • The object that receives IGListAdapterMoveDelegate events resulting from interactive reordering of sections.

    Note

    This works with UICollectionView interactive reordering available on iOS 9.0+

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListAdapterMoveDelegate> moveDelegate;

    Swift

    weak var moveDelegate: (any ListAdapterMoveDelegate)? { get set }
  • The object that receives IGListAdapterPerformanceDelegate events to measure performance.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListAdapterPerformanceDelegate> performanceDelegate;

    Swift

    weak var performanceDelegate: (any ListAdapterPerformanceDelegate)? { get set }
  • The updater for the adapter.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) id<IGListUpdatingDelegate> _Nonnull updater;

    Swift

    var updater: any IGListUpdatingDelegate { get }
  • A bitmask of experiments to conduct on the adapter.

    Declaration

    Objective-C

    @property (nonatomic) IGListExperiment experiments;

    Swift

    var experiments: IGListExperiment { get set }
  • When true, we automatically deselect cells after they are selected (animated), so each section-controller doesn’t have to. Note this doesn’t apply to selection via -selectItemAtIndexPath, since those do not receive a delegate callback. Default is false.

    Declaration

    Objective-C

    @property (nonatomic) BOOL autoDeselectEnabled;

    Swift

    var autoDeselectEnabled: Bool { get set }
  • Initializes a new IGListAdapter object.

    Note

    The working range is the number of objects beyond the visible objects (plus and minus) that should be notified when they are close to being visible. For instance, if you have 3 objects on screen and a working range of 2, the previous and succeeding 2 objects will be notified that they are within the working range. As you scroll the list the range is updated as objects enter and exit the working range.

    To opt out of using the working range, use initWithUpdater:viewController: or provide a working range of 0.

    Declaration

    Objective-C

    - (nonnull instancetype)
         initWithUpdater:(nonnull id<IGListUpdatingDelegate>)updater
          viewController:(nullable UIViewController *)viewController
        workingRangeSize:(NSInteger)workingRangeSize;

    Swift

    init(updater: any IGListUpdatingDelegate, viewController: UIViewController?, workingRangeSize: Int)

    Parameters

    updater

    An object that manages updates to the collection view.

    viewController

    The view controller that will house the adapter.

    workingRangeSize

    The number of objects before and after the viewport to consider within the working range.

    Return Value

    A new list adapter object.

  • Initializes a new IGListAdapter object with a working range of 0.

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithUpdater:(nonnull id<IGListUpdatingDelegate>)updater
         viewController:(nullable UIViewController *)viewController;

    Swift

    convenience init(updater: any IGListUpdatingDelegate, viewController: UIViewController?)

    Parameters

    updater

    An object that manages updates to the collection view.

    viewController

    The view controller that will house the adapter.

    Return Value

    A new list adapter object.

  • Perform an update from the previous state of the data source. This is analogous to calling -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)performUpdatesAnimated:(BOOL)animated
                        completion:(nullable IGListUpdaterCompletion)completion;

    Swift

    func performUpdates(animated: Bool, completion: ListUpdaterCompletion? = nil)

    Parameters

    animated

    A flag indicating if the transition should be animated.

    completion

    The block to execute when the updates complete.

  • Perform an immediate reload of the data in the data source, discarding the old objects.

    Warning

    Do not use this method to update without animations as it can be very expensive to teardown and rebuild all section controllers. Use -[IGListAdapter performUpdatesAnimated:completion] instead.

    Declaration

    Objective-C

    - (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion;

    Swift

    func reloadData(completion: ListUpdaterCompletion? = nil)

    Parameters

    completion

    The block to execute when the reload completes.

  • Reload the list for only the specified objects.

    Declaration

    Objective-C

    - (void)reloadObjects:(nonnull NSArray *)objects;

    Swift

    func reloadObjects(_ objects: [Any])

    Parameters

    objects

    The objects to reload.

  • Query the section controller at a given section index. Constant time lookup.

    Declaration

    Objective-C

    - (nullable IGListSectionController *)sectionControllerForSection:
        (NSInteger)section;

    Swift

    func sectionController(forSection section: Int) -> IGListSectionController?

    Parameters

    section

    A section in the list.

    Return Value

    A section controller or nil if the section does not exist.

  • Query the section index of a list. Constant time lookup.

    Declaration

    Objective-C

    - (NSInteger)sectionForSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func section(for sectionController: IGListSectionController) -> Int

    Parameters

    sectionController

    A list object.

    Return Value

    The section index of the list if it exists, otherwise NSNotFound.

  • Returns the section controller for the specified object. Constant time lookup.

    Declaration

    Objective-C

    - (__kindof IGListSectionController *_Nullable)sectionControllerForObject:
        (nonnull id)object;

    Swift

    func sectionController(for object: Any) -> IGListSectionController?

    Parameters

    object

    An object from the data source.

    Return Value

    A section controller or nil if object is not in the list.

  • Returns the object corresponding to the specified section controller in the list. Constant time lookup.

    Declaration

    Objective-C

    - (nullable id)objectForSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func object(for sectionController: IGListSectionController) -> Any?

    Parameters

    sectionController

    A section controller in the list.

    Return Value

    The object for the specified section controller, or nil if not found.

  • Returns the object corresponding to a section in the list. Constant time lookup.

    Declaration

    Objective-C

    - (nullable id)objectAtSection:(NSInteger)section;

    Swift

    func object(atSection section: Int) -> Any?

    Parameters

    section

    A section in the list.

    Return Value

    The object for the specified section, or nil if the section does not exist.

  • Returns the section corresponding to the specified object in the list. Constant time lookup.

    Declaration

    Objective-C

    - (NSInteger)sectionForObject:(nonnull id)object;

    Swift

    func section(for object: Any) -> Int

    Parameters

    object

    An object in the list.

    Return Value

    The section index of object if found, otherwise NSNotFound.

  • Returns a copy of all the objects currently driving the adapter.

    Declaration

    Objective-C

    - (nonnull NSArray *)objects;

    Swift

    func objects() -> [Any]

    Return Value

    An array of objects.

  • An unordered array of the currently visible section controllers.

    Declaration

    Objective-C

    - (nonnull NSArray<IGListSectionController *> *)visibleSectionControllers;

    Swift

    func visibleSectionControllers() -> [IGListSectionController]

    Return Value

    An array of section controllers.

  • An unordered array of the currently visible objects.

    Declaration

    Objective-C

    - (nonnull NSArray *)visibleObjects;

    Swift

    func visibleObjects() -> [Any]

    Return Value

    An array of objects

  • Less performant that visibleObjects but gives you an ordering to the list of visible objects in self.objects.

    Declaration

    Objective-C

    - (nonnull NSIndexSet *)indexesOfVisibleObjects;

    Swift

    func indexesOfVisibleObjects() -> IndexSet

    Return Value

    An index set for objects in self.objects. Result’s .count will be 0 if no visible objects.

  • An unordered array of the currently visible cells for a given object.

    Declaration

    Objective-C

    - (nonnull NSArray<UICollectionViewCell *> *)visibleCellsForObject:
        (nonnull id)object;

    Swift

    func visibleCells(for object: Any) -> [UICollectionViewCell]

    Parameters

    object

    An object in the list

    Return Value

    An array of collection view cells.

  • Scrolls to the specified object in the list adapter.

    Note

    The additional offset amount is to shift the final scroll position by some horizontal or vertical amount depending on the scroll direction. This is necessary when scrolling to an object on a view with sticky headers, since the sticky header would otherwise cover the top portion of the object.

    Declaration

    Objective-C

    - (void)scrollToObject:(nonnull id)object
        supplementaryKinds:(nullable NSArray<NSString *> *)supplementaryKinds
           scrollDirection:(UICollectionViewScrollDirection)scrollDirection
            scrollPosition:(UICollectionViewScrollPosition)scrollPosition
          additionalOffset:(CGFloat)additionalOffset
                  animated:(BOOL)animated;

    Swift

    func scroll(to object: Any, supplementaryKinds: [String]?, scrollDirection: UICollectionView.ScrollDirection, scrollPosition: UICollectionView.ScrollPosition, additionalOffset: CGFloat, animated: Bool)

    Parameters

    object

    The object to which to scroll.

    supplementaryKinds

    The types of supplementary views in the section.

    scrollDirection

    An option indicating the direction to scroll.

    scrollPosition

    An option that specifies where the item should be positioned when scrolling finishes.

    additionalOffset

    Additional offset amount from the scroll position.

    animated

    A flag indicating if the scrolling should be animated.

  • Returns the index path for the first visible cell that has been scrolled to. This refers to the cell currently at the top/left (0, 0) of the collection view’s frame, inset by the collection view’s contentInset top or left value if defined.

    Declaration

    Objective-C

    - (nullable NSIndexPath *)indexPathForFirstVisibleItem;

    Swift

    func indexPathForFirstVisibleItem() -> IndexPath?

    Return Value

    The index path of the cell or nil if not found.

  • Gets the scroll offset of the first visible cell scrolled into in the collection view. This refers to the cell currently at the top/left (0, 0) of the collection view’s frame, inset by the collection view’s contentInset top or left value if defined.

    Declaration

    Objective-C

    - (CGFloat)offsetForFirstVisibleItemWithScrollDirection:
        (UICollectionViewScrollDirection)scrollDirection;

    Swift

    func offsetForFirstVisibleItem(with scrollDirection: UICollectionView.ScrollDirection) -> CGFloat

    Parameters

    scrollDirection

    An option indicating the direction to scroll.

    Return Value

    additionalOffset is the offset amount the first visible cell is shifted from the start of the cell, the amount it has been scrolled into in the coordinates of the cell’s bounds.

  • Returns the size of a cell at the specified index path.

    Declaration

    Objective-C

    - (CGSize)sizeForItemAtIndexPath:(nonnull NSIndexPath *)indexPath;

    Swift

    func sizeForItem(at indexPath: IndexPath) -> CGSize

    Parameters

    indexPath

    The index path of the cell. å

    Return Value

    The size of the cell.

  • Returns the size of a supplementary view in the list at the specified index path.

    Declaration

    Objective-C

    - (CGSize)sizeForSupplementaryViewOfKind:(nonnull NSString *)elementKind
                                 atIndexPath:(nonnull NSIndexPath *)indexPath;

    Swift

    func sizeForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> CGSize

    Parameters

    elementKind

    The kind of supplementary view.

    indexPath

    The index path of the supplementary view.

    Return Value

    The size of the supplementary view.

  • Adds a listener to the list adapter.

    Note

    Listeners are held weakly so there is no need to call -[IGListAdapter removeUpdateListener:] on dealloc.

    Declaration

    Objective-C

    - (void)addUpdateListener:
        (nonnull id<IGListAdapterUpdateListener>)updateListener;

    Swift

    func add(_ updateListener: any ListAdapterUpdateListener)

    Parameters

    updateListener

    The object conforming to the IGListAdapterUpdateListener protocol.

  • Removes a listener from the list adapter.

    Declaration

    Objective-C

    - (void)removeUpdateListener:
        (nonnull id<IGListAdapterUpdateListener>)updateListener;

    Swift

    func remove(_ updateListener: any ListAdapterUpdateListener)

    Parameters

    updateListener

    The object conforming to the IGListAdapterUpdateListener protocol.

================================================ FILE: docs/Classes/IGListAdapterDelegateAnnouncer.html ================================================ IGListAdapterDelegateAnnouncer Class Reference

IGListAdapterDelegateAnnouncer

Objective-C

@interface IGListAdapterDelegateAnnouncer : NSObject

/// Default announcer for all `IGListAdapter`
+ (instancetype)sharedInstance;

/// Add a delegate that will receive callbacks for all `IGListAdapter`.
/// This is a weak reference, so you don't need to remove it on dealloc.
- (void)addListener:(id<IGListAdapterDelegate>)listener;

/// Remove delegate
- (void)removeListener:(id<IGListAdapterDelegate>)listener;

@end

Swift

class IGListAdapterDelegateAnnouncer : NSObject

Undocumented

  • Default announcer for all IGListAdapter

    Declaration

    Objective-C

    + (nonnull instancetype)sharedInstance;

    Swift

    class func sharedInstance() -> Self
  • Add a delegate that will receive callbacks for all IGListAdapter. This is a weak reference, so you don’t need to remove it on dealloc.

    Declaration

    Objective-C

    - (void)addListener:(nonnull id<IGListAdapterDelegate>)listener;

    Swift

    func addListener(_ listener: any ListAdapterDelegate)
  • Remove delegate

    Declaration

    Objective-C

    - (void)removeListener:(nonnull id<IGListAdapterDelegate>)listener;

    Swift

    func removeListener(_ listener: any ListAdapterDelegate)
================================================ FILE: docs/Classes/IGListAdapterUpdater.html ================================================ IGListAdapterUpdater Class Reference

IGListAdapterUpdater

Objective-C


@interface IGListAdapterUpdater : NSObject <IGListUpdatingDelegate>

Swift

@MainActor class ListAdapterUpdater : NSObject, ListUpdatingDelegate

An IGListAdapterUpdater is a concrete type that conforms to IGListUpdatingDelegate. It is an out-of-box updater for IGListAdapter objects to use.

Note

This updater performs re-entrant, coalesced updating for a list. It also uses a least-minimal diff for calculating UI updates when IGListAdapter calls -performUpdateWithCollectionView:fromObjects:toObjects:completion:.
  • The delegate that receives events with data on the performance of a transition.

    Declaration

    Objective-C

    @property (nonatomic, weak) id<IGListAdapterUpdaterDelegate> _Nullable delegate;

    Swift

    weak var delegate: (any ListAdapterUpdaterDelegate)? { get set }
  • A flag indicating if a section move should be treated as a section “delete, then insert” operation. This can be useful if you’re performing a lot of updates and moves are too distracting.

    Default is NO.

    Declaration

    Objective-C

    @property (nonatomic) BOOL sectionMovesAsDeletesInserts;

    Swift

    var sectionMovesAsDeletesInserts: Bool { get set }
  • ONLY used when there is N section, but each section only contains 1 item. We don’t need to change move into delete+insert, and we dont need to call -reload at all.

    This unlocks many default UICollectionView animations: move/inline cell updates/deletes/inserts etc.

    Default is NO.

    Warning

    This should only work for Section that ONLY has single item setup.

    Declaration

    Objective-C

    @property (nonatomic) BOOL singleItemSectionUpdates;

    Swift

    var singleItemSectionUpdates: Bool { get set }
  • A flag indicating that section reloads should be treated as item reloads, instead of converting them to “delete, then insert” operations. This only applies if the number of items for the section is unchanged.

    Default is NO.

    Note

    If the number of items for the section is changed, we would fallback to the default behavior and convert it to “delete + insert”, because the collectionView can crash otherwise.

    Declaration

    Objective-C

    @property (nonatomic) BOOL preferItemReloadsForSectionReloads;

    Swift

    var preferItemReloadsForSectionReloads: Bool { get set }
  • If there’s more than 100 diff updates, fallback to using reloadData to avoid stalling the main thread.

    Default is YES.

    Declaration

    Objective-C

    @property (nonatomic) BOOL allowsReloadingOnTooManyUpdates;

    Swift

    var allowsReloadingOnTooManyUpdates: Bool { get set }
  • Allow the diffing to be performed on a background thread.

    Default is NO.

    Declaration

    Objective-C

    @property (nonatomic) BOOL allowsBackgroundDiffing;

    Swift

    var allowsBackgroundDiffing: Bool { get set }
  • A bitmask of experiments to conduct on the updater.

    Declaration

    Objective-C

    @property (nonatomic) IGListExperiment experiments;

    Swift

    var experiments: IGListExperiment { get set }
  • This is an experimental feature to customize how diffing is performed. If there’s clear value, we’ll make this a real API and make breaking changes (i.e. replace allowsBackgroundDiffing with an enum).

    Warning

    - Keep in mind allowsBackgroundDiffing needs to be true for adaptive diffing to take effect.

    Declaration

    Objective-C

    @property (nonatomic) IGListAdaptiveDiffingExperimentConfig adaptiveDiffingExperimentConfig;

    Swift

    var adaptiveDiffingExperimentConfig: IGListAdaptiveDiffingExperimentConfig { get set }
  • Customize how coalescing works to speed up some updates

    Declaration

    Objective-C

    @property (nonatomic) IGListAdaptiveCoalescingExperimentConfig adaptiveCoalescingExperimentConfig;

    Swift

    var adaptiveCoalescingExperimentConfig: IGListAdaptiveCoalescingExperimentConfig { get set }
================================================ FILE: docs/Classes/IGListBatchUpdateData.html ================================================ IGListBatchUpdateData Class Reference

IGListBatchUpdateData

Objective-C


@interface IGListBatchUpdateData : NSObject

Swift

class ListBatchUpdateData : NSObject

An instance of IGListBatchUpdateData takes section indexes and item index paths and performs cleanup on init in order to perform a crash-free update via -[UICollectionView performBatchUpdates:completion:].

  • Section insert indexes.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexSet *_Nonnull insertSections;

    Swift

    var insertSections: IndexSet { get }
  • Section delete indexes.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexSet *_Nonnull deleteSections;

    Swift

    var deleteSections: IndexSet { get }
  • Section moves.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSSet<IGListMoveIndex *> *_Nonnull moveSections;

    Swift

    var moveSections: Set<ListMoveIndex> { get }
  • Item insert index paths.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSArray<NSIndexPath *> *_Nonnull insertIndexPaths;

    Swift

    var insertIndexPaths: [IndexPath] { get }
  • Item delete index paths.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSArray<NSIndexPath *> *_Nonnull deleteIndexPaths;

    Swift

    var deleteIndexPaths: [IndexPath] { get }
  • Item update index paths.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSArray<NSIndexPath *> *_Nonnull updateIndexPaths;

    Swift

    var updateIndexPaths: [IndexPath] { get }
  • Item moves.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSArray<IGListMoveIndexPath *> *_Nonnull moveIndexPaths;

    Swift

    var moveIndexPaths: [ListMoveIndexPath] { get }
  • Creates a new batch update object with section and item operations.

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithInsertSections:(nonnull NSIndexSet *)insertSections
                deleteSections:(nonnull NSIndexSet *)deleteSections
                  moveSections:(nonnull NSSet<IGListMoveIndex *> *)moveSections
              insertIndexPaths:(nonnull NSArray<NSIndexPath *> *)insertIndexPaths
              deleteIndexPaths:(nonnull NSArray<NSIndexPath *> *)deleteIndexPaths
              updateIndexPaths:(nonnull NSArray<NSIndexPath *> *)updateIndexPaths
                moveIndexPaths:
                    (nonnull NSArray<IGListMoveIndexPath *> *)moveIndexPaths;

    Swift

    init(insertSections: IndexSet, deleteSections: IndexSet, moveSections: Set<ListMoveIndex>, insert insertIndexPaths: [IndexPath], delete deleteIndexPaths: [IndexPath], update updateIndexPaths: [IndexPath], moveIndexPaths: [ListMoveIndexPath])

    Parameters

    insertSections

    Section indexes to insert.

    deleteSections

    Section indexes to delete.

    moveSections

    Section moves.

    insertIndexPaths

    Item index paths to insert.

    deleteIndexPaths

    Item index paths to delete.

    updateIndexPaths

    Item index paths to update.

    moveIndexPaths

    Item index paths to move.

    Return Value

    A new batch update object.

================================================ FILE: docs/Classes/IGListBindingSectionController.html ================================================ IGListBindingSectionController Class Reference

IGListBindingSectionController

Objective-C


@interface IGListBindingSectionController<
    __covariant ObjectType : id <IGListDiffable>> : IGListSectionController

Swift

@MainActor class ListBindingSectionController<ObjectType> : ListSectionController where ObjectType : ListDiffable

This section controller uses a data source to transform its “top level” object into an array of diffable view models. It then automatically binds each view model to cells via the IGListBindable protocol.

Models used with IGListBindingSectionController should take special care to always return YES for identical objects. That is, any objects with matching -diffIdentifiers should always be equal, that way the section controller can create new view models via the data source, create a diff, and update the specific cells that have changed.

In Objective-C, your -isEqualToDiffableObject: can simply be:

- (BOOL)isEqualToDiffableObject:(id)object {
  return YES;
}

In Swift:

func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
  return true
}

Only when -diffIdentifiers match is object equality compared, so you can assume the class is the same, and the instance has already been checked.

  • A data source that transforms a top-level object into view models, and returns cells and sizes for given view models.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListBindingSectionControllerDataSource> dataSource;

    Swift

    weak var dataSource: (any ListBindingSectionControllerDataSource)? { get set }
  • A delegate that receives selection events from cells in an IGListBindingSectionController instance.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListBindingSectionControllerSelectionDelegate> selectionDelegate;

    Swift

    weak var selectionDelegate: (any ListBindingSectionControllerSelectionDelegate)? { get set }
  • The object currently assigned to the section controller, if any.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly, nullable) ObjectType object;

    Swift

    var object: ObjectType? { get }
  • The array of view models created from the data source. Values are changed when the top-level object changes or by calling -updateAnimated:completion: manually.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<id<IGListDiffable>> *_Nonnull viewModels;

    Swift

    var viewModels: [any ListDiffable] { get }
  • Tells the section controller to query for new view models, diff the changes, and update its cells.

    Declaration

    Objective-C

    - (void)updateAnimated:(BOOL)animated
                completion:(nullable void (^)(BOOL))completion;

    Swift

    func update(animated: Bool) async -> Bool

    Parameters

    animated

    A flag indicating if the transition should be animated or not.

    completion

    An optional completion block executed after updates finish. Parameter is YES if updates were applied.

  • Notifies the section that a list object should move within a section as the result of interactive reordering.

    Note

    this method must be implemented if interactive reordering is enabled. To ensure updating the internal viewModels array, calling super is required, preferably before your own implementation.

    Declaration

    Objective-C

    - (void)moveObjectFromIndex:(NSInteger)sourceIndex
                        toIndex:(NSInteger)destinationIndex;

    Swift

    func moveObject(from sourceIndex: Int, to destinationIndex: Int)

    Parameters

    sourceIndex

    The starting index of the object.

    destinationIndex

    The ending index of the object.

================================================ FILE: docs/Classes/IGListBindingSingleSectionController.html ================================================ IGListBindingSingleSectionController Class Reference

IGListBindingSingleSectionController

Objective-C


@interface IGListBindingSingleSectionController<
    __covariant ViewModel : id <IGListDiffable>, Cell : UICollectionViewCell *>
    : IGListSectionController

Swift

class ListBindingSingleSectionController<ViewModel, Cell> : ListSectionController where ViewModel : ListDiffable, Cell : UICollectionViewCell

Special section controller that only contains a single item, and it will apply the view model update during -didUpdateObject: call, usually happened inside -[UICollectionView performBatchUpdates:completion:].

This class is intended to be subclassed.

Subclass

  • Undocumented

    Declaration

    Objective-C

    - (Class)cellClass;

    Swift

    func cellClass() -> AnyClass
  • Undocumented

    Declaration

    Objective-C

    - (void)configureCell:(Cell)cell withViewModel:(ViewModel)viewModel;

    Swift

    func configureCell(_ cell: Cell, withViewModel viewModel: ViewModel)
  • Undocumented

    Declaration

    Objective-C

    - (CGSize)sizeForViewModel:(ViewModel)viewModel;

    Swift

    func size(forViewModel viewModel: ViewModel) -> CGSize
  • Undocumented

    Declaration

    Objective-C

    - (void)didSelectItemWithCell:(Cell)cell;

    Swift

    func didSelectItem(with cell: Cell)
  • Undocumented

    Declaration

    Objective-C

    - (void)didDeselectItemWithCell:(Cell)cell;

    Swift

    func didDeselectItem(with cell: Cell)
  • Undocumented

    Declaration

    Objective-C

    - (void)didHighlightItemWithCell:(Cell)cell;

    Swift

    func didHighlightItem(with cell: Cell)
  • Undocumented

    Declaration

    Objective-C

    - (void)didUnhighlightItemWithCell:(Cell)cell;

    Swift

    func didUnhighlightItem(with cell: Cell)
  • Undocumented

    Declaration

    Objective-C

    - (BOOL)isDisplayingCell;

    Swift

    func isDisplayingCell() -> Bool
================================================ FILE: docs/Classes/IGListCollectionView.html ================================================ IGListCollectionView Class Reference

IGListCollectionView

Objective-C


@interface IGListCollectionView : UICollectionView

Swift

class ListCollectionView : UICollectionView

This UICollectionView subclass allows for partial layout invalidation using IGListCollectionViewLayout, or custom layout classes that conform to IGListCollectionViewLayoutCompatible.

Note

When updating a collection view (ex: calling -insertSections), -invalidateLayoutWithContext gets called on the layout object. However, the invalidation context doesn’t provide details on which index paths are being modified, which typically forces a full layout re-calculation. IGListCollectionView gives IGListCollectionViewLayout the missing information to re-calculate only the modified layout attributes.
  • Create a new view with an IGListcollectionViewLayout class or subclass.

    Note

    You can initialize a new view with a base layout by simply calling -[IGListCollectionView initWithFrame:].

    Declaration

    Objective-C

    - (nonnull instancetype)
                   initWithFrame:(CGRect)frame
        listCollectionViewLayout:
            (nonnull UICollectionViewLayout<IGListCollectionViewLayoutCompatible> *)
                collectionViewLayout;

    Swift

    init(frame: CGRect, listCollectionViewLayout collectionViewLayout: any UICollectionViewLayout & IGListCollectionViewLayoutCompatible)

    Parameters

    frame

    The frame to initialize with.

    collectionViewLayout

    The layout to use with the collection view. You can use IGListCollectionViewLayout here, or a custom layout class that conforms to IGListCollectionViewLayoutCompatible.

================================================ FILE: docs/Classes/IGListCollectionViewLayout.html ================================================ IGListCollectionViewLayout Class Reference

IGListCollectionViewLayout

Objective-C


@interface IGListCollectionViewLayout
    : UICollectionViewLayout <IGListCollectionViewLayoutCompatible>

Swift

class ListCollectionViewLayout : UICollectionViewLayout, ListCollectionViewLayoutCompatible

This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, care must be taken not to stall the main thread in sizing delegate methods.

This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that:

  • Your UICollectionView data source must also conform to UICollectionViewDelegateFlowLayout
  • Header support given via UICollectionElementKindSectionHeader

All UICollectionViewDelegateFlowLayout methods are required and used by this layout:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;

In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position of an item extends beyond the width of the collection view. When that happens, the item is “newlined” to the next row. The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row.

Ex. of a section (2,0) with a large width causing a newline.

|[ 0,0 ][ 1,0 ]         |
|[         2,0         ]|

A section with a non-zero height header will always cause that section to newline. Headers are always stretched to the width of the collection view, pinched with the section insets.

Ex. of a section (2,0) with a header inset on the left/right.

|[ 0,0 ][ 1,0 ]         |
| >======header=======< |
| [ 2,0 ]               |

Section insets apply to items in the section no matter if they begin on a new row or are on the same row as a previous section.

Ex. of a section (2) with multiple items and a left inset.

|[ 0,0 ][ 1,0 ] >[ 2,0 ]|
| >[ 2,1 ][ 2,2 ][ 2,3 ]|

Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same section.

In a horizontally scrolling layout, sections and items are flowed vertically until they need to be “newlined” to the next column. Headers, if used, are stretched to the height of the collection view, minus the section insets.

Please see the unit tests for more configuration examples and expected output.

  • Direction in which layout will be scrollable; items will be flowed in the perpendicular direction, “newlining” when they run out of space along that axis or when a non-zero header is found.

    Declaration

    Objective-C

    @property (nonatomic, readonly) UICollectionViewScrollDirection scrollDirection;

    Swift

    var scrollDirection: UICollectionView.ScrollDirection { get }
  • Set this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky header position as UI like the navigation bar is scrolled offscreen. In a vertically scrolling layout, changing this to the height of the navigation bar will give the effect of the headers sticking to the nav as it is collapsed.

    Note

    Changing the value on this method will invalidate the layout every time.

    Declaration

    Objective-C

    @property (nonatomic) CGFloat stickyHeaderYOffset;

    Swift

    var stickyHeaderYOffset: CGFloat { get set }
  • Set this to YES to show sticky header when a section had no item. Default is NO.

    Declaration

    Objective-C

    @property (nonatomic) BOOL showHeaderWhenEmpty;

    Swift

    var showHeaderWhenEmpty: Bool { get set }
  • Set this to YES to keep layout cache when invalidateLayout is called. Default is NO.

    Declaration

    Objective-C

    @property (nonatomic) BOOL preserveLayoutCacheOnInvalidateLayout;

    Swift

    var preserveLayoutCacheOnInvalidateLayout: Bool { get set }
  • Create and return a new collection view layout.

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithStickyHeaders:(BOOL)stickyHeaders
              scrollDirection:(UICollectionViewScrollDirection)scrollDirection
              topContentInset:(CGFloat)topContentInset
                stretchToEdge:(BOOL)stretchToEdge;

    Swift

    init(stickyHeaders: Bool, scrollDirection: UICollectionView.ScrollDirection, topContentInset: CGFloat, stretchToEdge: Bool)

    Parameters

    stickyHeaders

    Set to YES to stick section headers to the top of the bounds while scrolling.

    scrollDirection

    Direction along which the collection view will be scrollable (if content size exceeds the frame size)

    topContentInset

    The content inset (top or left, depending on scrolling direction) used to offset the sticky headers. Ignored if stickyHeaders is NO.

    stretchToEdge

    Specifies whether to stretch width (in vertically scrolling layout) or height (horizontally scrolling) of last item to right/bottom edge when distance from last item to right/bottom edge < epsilon(1)

    Return Value

    A new collection view layout.

  • Create and return a new vertically scrolling collection view layout.

    Declaration

    Objective-C

    - (nonnull instancetype)initWithStickyHeaders:(BOOL)stickyHeaders
                                  topContentInset:(CGFloat)topContentInset
                                    stretchToEdge:(BOOL)stretchToEdge;

    Swift

    convenience init(stickyHeaders: Bool, topContentInset: CGFloat, stretchToEdge: Bool)

    Parameters

    stickyHeaders

    Set to YES to stick section headers to the top of the bounds while scrolling.

    topContentInset

    The top content inset used to offset the sticky headers. Ignored if stickyHeaders is NO.

    stretchToEdge

    Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1)

    Return Value

    A new collection view layout.

================================================ FILE: docs/Classes/IGListCollectionViewLayoutInvalidationContext.html ================================================ IGListCollectionViewLayoutInvalidationContext Class Reference

IGListCollectionViewLayoutInvalidationContext

Objective-C


@interface IGListCollectionViewLayoutInvalidationContext
    : UICollectionViewLayoutInvalidationContext

Swift

class ListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext

The default invalidation context class used by IGListCollectionViewLayout.

  • False by default. If true, supplementary list item attributes (headers and footers) will be invalidated.

    Declaration

    Objective-C

    @property (nonatomic) BOOL invalidateSupplementaryListAttributes;

    Swift

    var invalidateSupplementaryListAttributes: Bool { get set }
  • False by default. If true, all list item attributes will be invalidated.

    Declaration

    Objective-C

    @property (nonatomic) BOOL invalidateAllListAttributes;

    Swift

    var invalidateAllListAttributes: Bool { get set }
================================================ FILE: docs/Classes/IGListGenericSectionController.html ================================================ IGListGenericSectionController Class Reference

IGListGenericSectionController

Objective-C


@interface IGListGenericSectionController<__covariant ObjectType>
    : IGListSectionController

Swift

class ListGenericSectionController<ObjectType> : ListSectionController where ObjectType : AnyObject

This class adds a helper layer to IGListSectionController to automatically store a generic object in didUpdateToObject:.

  • The object mapped to this section controller. Matches the object provided in [IGListAdapterDataSource listAdapter:sectionControllerForObject:] when this section controller was created and returned.

    Note

    This object is briefly nil between initialization and the first call to didUpdateToObject:. After that, it is safe to assume that this is non-nil. For this reason, we bridge it to Swift as an implicitly-unwrapped Optional, so that idiomatic IGListKit code is not forced to handle nullability with explicit as! or fatalError.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly, null_unspecified) ObjectType object;

    Swift

    var object: ObjectType! { get }
  • Updates the section controller to a new object.

    Note

    This IGListSectionController subclass sets its object in this method, so any overrides must call super.

    Declaration

    Objective-C

    - (void)didUpdateToObject:(nonnull ObjectType)object;

    Swift

    func didUpdate(to object: ObjectType)

    Parameters

    object

    The object mapped to this section controller.

================================================ FILE: docs/Classes/IGListIndexPathResult.html ================================================ IGListIndexPathResult Class Reference

IGListIndexPathResult

Objective-C


@interface IGListIndexPathResult : NSObject

Swift

class ListIndexPathResult : NSObject

A result object returned when diffing with sections.

  • The index paths inserted into the new collection.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *_Nonnull inserts;

    Swift

    var inserts: [IndexPath] { get }
  • The index paths deleted from the old collection.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *_Nonnull deletes;

    Swift

    var deletes: [IndexPath] { get }
  • The index paths in the old collection that need updated.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *_Nonnull updates;

    Swift

    var updates: [IndexPath] { get }
  • The moves from an index path in the old collection to an index path in the new collection.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<IGListMoveIndexPath *> *_Nonnull moves;

    Swift

    var moves: [ListMoveIndexPath] { get }
  • A Read-only boolean that indicates whether the result has any changes or not. YES if the result has changes, NO otherwise.

    Declaration

    Objective-C

    @property (nonatomic, readonly) BOOL hasChanges;

    Swift

    var hasChanges: Bool { get }
  • Returns the index path of the object with the specified identifier before the diff.

    Declaration

    Objective-C

    - (nullable NSIndexPath *)oldIndexPathForIdentifier:
        (nonnull id<NSObject>)identifier;

    Swift

    func oldIndexPath(forIdentifier identifier: any NSObjectProtocol) -> IndexPath?

    Parameters

    identifier

    The diff identifier of the object.

    Return Value

    The index path of the object before the diff, or nil.

  • Returns the index path of the object with the specified identifier after the diff.

    Declaration

    Objective-C

    - (nullable NSIndexPath *)newIndexPathForIdentifier:
        (nonnull id<NSObject>)identifier;

    Swift

    func newIndexPath(forIdentifier identifier: any NSObjectProtocol) -> IndexPath?

    Parameters

    identifier

    The diff identifier of the object.

    Return Value

    The index path of the object after the diff, or nil.

  • Creates a new result object with operations safe for use in UITableView and UICollectionView batch updates.

    Declaration

    Objective-C

    - (nonnull IGListIndexPathResult *)resultForBatchUpdates;

    Swift

    func forBatchUpdates() -> ListIndexPathResult
================================================ FILE: docs/Classes/IGListIndexSetResult.html ================================================ IGListIndexSetResult Class Reference

IGListIndexSetResult

Objective-C


@interface IGListIndexSetResult : NSObject

Swift

class ListIndexSetResult : NSObject

A result object returned when diffing with indexes.

  • The indexes inserted into the new collection.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexSet *_Nonnull inserts;

    Swift

    var inserts: IndexSet { get }
  • The indexes deleted from the old collection.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexSet *_Nonnull deletes;

    Swift

    var deletes: IndexSet { get }
  • The indexes in the old collection that need updated.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexSet *_Nonnull updates;

    Swift

    var updates: IndexSet { get }
  • The moves from an index in the old collection to an index in the new collection.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<IGListMoveIndex *> *_Nonnull moves;

    Swift

    var moves: [ListMoveIndex] { get }
  • A Read-only boolean that indicates whether the result has any changes or not. YES if the result has changes, NO otherwise.

    Declaration

    Objective-C

    @property (nonatomic, readonly) BOOL hasChanges;

    Swift

    var hasChanges: Bool { get }
  • Returns the index of the object with the specified identifier before the diff.

    Declaration

    Objective-C

    - (NSInteger)oldIndexForIdentifier:(nonnull id<NSObject>)identifier;

    Swift

    func oldIndex(forIdentifier identifier: any NSObjectProtocol) -> Int

    Parameters

    identifier

    The diff identifier of the object.

    Return Value

    The index of the object before the diff, or NSNotFound.

  • Returns the index of the object with the specified identifier after the diff.

    Declaration

    Objective-C

    - (NSInteger)newIndexForIdentifier:(nonnull id<NSObject>)identifier;

    Swift

    func newIndex(forIdentifier identifier: any NSObjectProtocol) -> Int

    Parameters

    identifier

    The diff identifier of the object.

    Return Value

    The index path of the object after the diff, or NSNotFound.

  • Creates a new result object with operations safe for use in UITableView and UICollectionView batch updates.

    Declaration

    Objective-C

    - (nonnull IGListIndexSetResult *)resultForBatchUpdates;

    Swift

    func forBatchUpdates() -> ListIndexSetResult
================================================ FILE: docs/Classes/IGListMoveIndex.html ================================================ IGListMoveIndex Class Reference

IGListMoveIndex

Objective-C


@interface IGListMoveIndex : NSObject

Swift

class ListMoveIndex : NSObject

An object representing a move between indexes.

  • An index in the old collection.

    Declaration

    Objective-C

    @property (nonatomic, readonly) NSInteger from;

    Swift

    var from: Int { get }
  • to

    An index in the new collection.

    Declaration

    Objective-C

    @property (nonatomic, readonly) NSInteger to;

    Swift

    var to: Int { get }
================================================ FILE: docs/Classes/IGListMoveIndexPath.html ================================================ IGListMoveIndexPath Class Reference

IGListMoveIndexPath

Objective-C


@interface IGListMoveIndexPath : NSObject

Swift

class ListMoveIndexPath : NSObject

An object representing a move between indexes.

  • An index path in the old collection.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexPath *_Nonnull from;

    Swift

    var from: IndexPath { get }
  • to

    An index path in the new collection.

    Declaration

    Objective-C

    @property (nonatomic, strong, readonly) NSIndexPath *_Nonnull to;

    Swift

    var to: IndexPath { get }
================================================ FILE: docs/Classes/IGListSectionController.html ================================================ IGListSectionController Class Reference

IGListSectionController

Objective-C


@interface IGListSectionController : NSObject

Swift

@MainActor class ListSectionController : NSObject

The base class for section controllers used in a list. This class is intended to be subclassed.

  • Returns the number of items in the section.

    Note

    The count returned is used to drive the number of cells displayed for this section controller. The default implementation returns 1. Calling super is not required.

    Declaration

    Objective-C

    - (NSInteger)numberOfItems;

    Swift

    func numberOfItems() -> Int

    Return Value

    A count of items in the list.

  • The specific size for the item at the specified index.

    Note

    The returned size is not guaranteed to be used. The implementation may query sections for their layout information at will, or use its own layout metrics. For example, consider a dynamic-text sized list versus a fixed height-and-width grid. The former will ask each section for a size, and the latter will likely not. The default implementation returns size zero. Calling super is not required.

    Declaration

    Objective-C

    - (CGSize)sizeForItemAtIndex:(NSInteger)index;

    Swift

    func sizeForItem(at index: Int) -> CGSize

    Parameters

    index

    The row index of the item.

    Return Value

    The size for the item at index.

  • Return a dequeued cell for a given index.

    Note

    This is your opportunity to do any cell setup and configuration. The infrastructure requests a cell when it will be used on screen. You should never allocate new cells in this method, instead use the provided adapter to call one of the dequeue methods on the IGListCollectionContext. The default implementation will assert. You must override this method without calling super.

    Warning

    Don’t call this method to obtain a reference to currently dequeued cells: a new cell will be dequeued and returned, rather than the existing cell that you may have intended to retrieve. Instead, you can call -cellForItemAtIndex:sectionController: on IGListCollectionContext to obtain active cell references.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index;

    Swift

    @MainActor func cellForItem(at index: Int) -> UICollectionViewCell

    Parameters

    index

    The index of the requested row.

    Return Value

    A configured UICollectionViewCell subclass.

  • Updates the section controller to a new object.

    Note

    When this method is called, all available contexts and configurations have been set for the section controller. This method will only be called when the object instance has changed, including from nil or a previous object. Calling super is not required.

    Declaration

    Objective-C

    - (void)didUpdateToObject:(nonnull id)object;

    Swift

    func didUpdate(to object: Any)

    Parameters

    object

    The object mapped to this section controller.

  • Asks the section controller if the cell at the specified index path should be selected

    Note

    The default implementation returns YES. Calling super is not required.

    Declaration

    Objective-C

    - (BOOL)shouldSelectItemAtIndex:(NSInteger)index;

    Swift

    func shouldSelectItem(at index: Int) -> Bool

    Parameters

    index

    The index of cell to be selected.

  • Asks the section controller if the cell at the specified index path should be deselected

    Note

    The default implementation returns YES. Calling super is not required.

    Declaration

    Objective-C

    - (BOOL)shouldDeselectItemAtIndex:(NSInteger)index;

    Swift

    func shouldDeselectItem(at index: Int) -> Bool

    Parameters

    index

    The index of cell to be deselected.

  • Tells the section controller that the cell at the specified index path was selected.

    Note

    The default implementation does nothing. Calling super is not required.

    Declaration

    Objective-C

    - (void)didSelectItemAtIndex:(NSInteger)index;

    Swift

    func didSelectItem(at index: Int)

    Parameters

    index

    The index of the selected cell.

  • Tells the section controller that the cell at the specified index path was deselected.

    Note

    The default implementation does nothing. Calling super is not required.

    Declaration

    Objective-C

    - (void)didDeselectItemAtIndex:(NSInteger)index;

    Swift

    func didDeselectItem(at index: Int)

    Parameters

    index

    The index of the deselected cell.

  • Tells the section controller that the cell at the specified index path was highlighted.

    Note

    The default implementation does nothing. Calling super is not required.

    Declaration

    Objective-C

    - (void)didHighlightItemAtIndex:(NSInteger)index;

    Swift

    func didHighlightItem(at index: Int)

    Parameters

    index

    The index of the highlighted cell.

  • Tells the section controller that the cell at the specified index path was unhighlighted.

    Note

    The default implementation does nothing. Calling super is not required.

    Declaration

    Objective-C

    - (void)didUnhighlightItemAtIndex:(NSInteger)index;

    Swift

    func didUnhighlightItem(at index: Int)

    Parameters

    index

    The index of the unhighlighted cell.

  • Tells the section controller that the cell has requested a menu configuration.

    Note

    The default implementation does nothing. Calling super is not required.

    Declaration

    Objective-C

    - (UIContextMenuConfiguration *_Nullable)
        contextMenuConfigurationForItemAtIndex:(NSInteger)index
                                         point:(CGPoint)point;

    Swift

    func contextMenuConfigurationForItem(at index: Int, point: CGPoint) -> UIContextMenuConfiguration?

    Parameters

    index

    The index of the cell that requested the menu.

    point

    The point of the tap on the cell.

    Return Value

    An object that conforms to UIContextMenuConfiguration

  • Identifies whether an object can be moved through interactive reordering.

    Note

    Interactive reordering is supported both for items within a single section, as well as for reordering sections themselves when sections contain only one item. The default implementation returns false.

    Declaration

    Objective-C

    - (BOOL)canMoveItemAtIndex:(NSInteger)index;

    Swift

    func canMoveItem(at index: Int) -> Bool

    Parameters

    index

    The index of the object in the list.

    Return Value

    YES if the object is allowed to move, otherwise NO.

  • Notifies the section that a list object should move within a section as the result of interactive reordering.

    Note

    this method must be implemented if interactive reordering is enabled.

    Declaration

    Objective-C

    - (void)moveObjectFromIndex:(NSInteger)sourceIndex
                        toIndex:(NSInteger)destinationIndex;

    Swift

    func moveObject(from sourceIndex: Int, to destinationIndex: Int)

    Parameters

    sourceIndex

    The starting index of the object.

    destinationIndex

    The ending index of the object.

  • The view controller housing the adapter that created this section controller.

    Note

    Use this view controller to push, pop, present, or do other custom transitions.

    Warning

    It is considered very bad practice to cast this to a known view controller and call methods on it other than for navigations and transitions.

    Declaration

    Objective-C

    @property (nonatomic, weak, readonly, nullable) UIViewController *viewController;

    Swift

    weak var viewController: UIViewController? { get }
  • A context object for interacting with the collection.

    Use this property for accessing the collection size, dequeuing cells, reloading, inserting, deleting, etc.

    Note

    When created outside of -listAdapter:sectionControllerForObject:, this object is temporarily nil after initialization. We bridge it to Swift as an implicitly-unwrapped Optional, so that idiomatic IGListKit code is not forced to handle nullability with explicit as! or fatalError, as using a non-nil instance of this object is essential for dequeueing cells.

    Declaration

    Objective-C

    @property (nonatomic, weak, readonly, null_unspecified) id<IGListCollectionContext> collectionContext;

    Swift

    weak var collectionContext: (any ListCollectionContext)! { get }
  • Returns the section within the list for this section controller.

    Note

    This value also relates to the section within a UICollectionView that this section controller’s cells belong. It also relates to the -[NSIndexPath section] value for individual cells within the collection view.

    Declaration

    Objective-C

    @property (nonatomic, readonly) NSInteger section;

    Swift

    var section: Int { get }
  • Returns YES if the section controller is the first section in the list, NO otherwise.

    Declaration

    Objective-C

    @property (nonatomic, readonly) BOOL isFirstSection;

    Swift

    var isFirstSection: Bool { get }
  • Returns YES if the section controller is the last section in the list, NO otherwise.

    Declaration

    Objective-C

    @property (nonatomic, readonly) BOOL isLastSection;

    Swift

    var isLastSection: Bool { get }
  • The margins used to lay out content in the section controller.

    See

    -[UICollectionViewFlowLayout sectionInset].

    Declaration

    Objective-C

    @property (nonatomic) UIEdgeInsets inset;

    Swift

    var inset: UIEdgeInsets { get set }
  • The minimum spacing to use between rows of items.

    See

    -[UICollectionViewFlowLayout minimumLineSpacing].

    Declaration

    Objective-C

    @property (nonatomic) CGFloat minimumLineSpacing;

    Swift

    var minimumLineSpacing: CGFloat { get set }
  • The minimum spacing to use between items in the same row.

    See

    -[UICollectionViewFlowLayout minimumInteritemSpacing].

    Declaration

    Objective-C

    @property (nonatomic) CGFloat minimumInteritemSpacing;

    Swift

    var minimumInteritemSpacing: CGFloat { get set }
  • The supplementary view source for the section controller. Can be nil.

    Note

    You may wish to return self if your section controller implements this protocol.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListSupplementaryViewSource> supplementaryViewSource;

    Swift

    weak var supplementaryViewSource: (any ListSupplementaryViewSource)? { get set }

    Return Value

    An object that conforms to IGListSupplementaryViewSource or nil.

  • An object that handles display events for the section controller. Can be nil.

    Note

    You may wish to return self if your section controller implements this protocol.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListDisplayDelegate> displayDelegate;

    Swift

    weak var displayDelegate: (any ListDisplayDelegate)? { get set }

    Return Value

    An object that conforms to IGListDisplayDelegate or nil.

  • An object that handles working range events for the section controller. Can be nil.

    Note

    You may wish to return self if your section controller implements this protocol.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListWorkingRangeDelegate> workingRangeDelegate;

    Swift

    weak var workingRangeDelegate: (any ListWorkingRangeDelegate)? { get set }

    Return Value

    An object that conforms to IGListWorkingRangeDelegate or nil.

  • An object that handles scroll events for the section controller. Can be nil.

    Note

    You may wish to return self if your section controller implements this protocol.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListScrollDelegate> scrollDelegate;

    Swift

    weak var scrollDelegate: (any ListScrollDelegate)? { get set }

    Return Value

    An object that conforms to IGListScrollDelegate or nil.

  • An object that handles transition events for the section controller. Can be nil.

    Note

    You may wish to return self if your section controller implements this protocol.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListTransitionDelegate> transitionDelegate;

    Swift

    weak var transitionDelegate: (any IGListTransitionDelegate)? { get set }

    Return Value

    An object that conforms to IGListTransitionDelegat or nil.

================================================ FILE: docs/Classes/IGListSingleSectionController.html ================================================ IGListSingleSectionController Class Reference

IGListSingleSectionController

Objective-C


@interface IGListSingleSectionController : IGListSectionController

Swift

class ListSingleSectionController : ListSectionController

This section controller is meant to make building simple, single-cell lists easier. By providing the type of cell, a block to configure the cell, and a block to return the size of a cell, you can use an IGListAdapter-powered list with a simpler architecture.

  • Creates a new section controller for a given cell type that will always have only one cell when present in a list.

    Warning

    Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually self) or the IGListAdapter. Pass in locally scoped objects or use weak references!

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithCellClass:(nonnull Class)cellClass
           configureBlock:
               (nonnull IGListSingleSectionCellConfigureBlock)configureBlock
                sizeBlock:(nonnull IGListSingleSectionCellSizeBlock)sizeBlock;

    Swift

    init(cellClass: AnyClass, configureBlock: @escaping ListSingleSectionCellConfigureBlock, sizeBlock: @escaping ListSingleSectionCellSizeBlock)

    Parameters

    cellClass

    The UICollectionViewCell subclass for the single cell.

    configureBlock

    A block that configures the cell with the item given to the section controller.

    sizeBlock

    A block that returns the size for the cell given the collection context.

    Return Value

    A new section controller.

  • Creates a new section controller for a given nib name and bundle that will always have only one cell when present in a list.

    Warning

    Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually self) or the IGListAdapter. Pass in locally scoped objects or use weak references!

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithNibName:(nonnull NSString *)nibName
                 bundle:(nullable NSBundle *)bundle
         configureBlock:
             (nonnull IGListSingleSectionCellConfigureBlock)configureBlock
              sizeBlock:(nonnull IGListSingleSectionCellSizeBlock)sizeBlock;

    Swift

    init(nibName: String, bundle: Bundle?, configureBlock: @escaping ListSingleSectionCellConfigureBlock, sizeBlock: @escaping ListSingleSectionCellSizeBlock)

    Parameters

    nibName

    The name of the nib file for the single cell.

    bundle

    The bundle in which to search for the nib file. If nil, this method looks for the file in the main bundle.

    configureBlock

    A block that configures the cell with the item given to the section controller.

    sizeBlock

    A block that returns the size for the cell given the collection context.

    Return Value

    A new section controller.

  • Creates a new section controller for a given storyboard cell identifier that will always have only one cell when present in a list.

    Warning

    Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter (usually self) or the IGListAdapter. Pass in locally scoped objects or use weak references!

    Declaration

    Objective-C

    - (nonnull instancetype)
        initWithStoryboardCellIdentifier:(nonnull NSString *)identifier
                          configureBlock:
                              (nonnull IGListSingleSectionCellConfigureBlock)
                                  configureBlock
                               sizeBlock:(nonnull IGListSingleSectionCellSizeBlock)
                                             sizeBlock;

    Swift

    init(storyboardCellIdentifier identifier: String, configureBlock: @escaping ListSingleSectionCellConfigureBlock, sizeBlock: @escaping ListSingleSectionCellSizeBlock)

    Parameters

    identifier

    The identifier of the cell prototype in storyboard.

    configureBlock

    A block that configures the cell with the item given to the section controller.

    sizeBlock

    A block that returns the size for the cell given the collection context.

    Return Value

    A new section controller.

  • An optional delegate that handles selection and deselection.

    Declaration

    Objective-C

    @property (nonatomic, weak, nullable) id<IGListSingleSectionControllerDelegate> selectionDelegate;

    Swift

    weak var selectionDelegate: (any ListSingleSectionControllerDelegate)? { get set }
================================================ FILE: docs/Classes/IGListTransitionData.html ================================================ IGListTransitionData Class Reference

IGListTransitionData

Objective-C


@interface IGListTransitionData : NSObject

Swift

class ListTransitionData : NSObject

Container object that holds the data needed for an update.

  • Undocumented

    Declaration

    Objective-C

    - (instancetype)initFromObjects:(NSArray *)fromObjects
                          toObjects:(NSArray *)toObjects
               toSectionControllers:(NSArray<IGListSectionController *> *)toSectionControllers NS_DESIGNATED_INITIALIZER;

    Swift

    init(from fromObjects: [Any], to toObjects: [Any], to toSectionControllers: [IGListSectionController])
  • Unavailable

    Undocumented

    Declaration

    Objective-C

    - (instancetype)init NS_UNAVAILABLE;
  • Unavailable

    Undocumented

    Declaration

    Objective-C

    + (instancetype)new NS_UNAVAILABLE;
  • The previous objects in the collection view. Objects must conform to IGListDiffable.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray *_Nonnull fromObjects;

    Swift

    var fromObjects: [Any] { get }
  • The new objects in the collection view. Objects must conform to IGListDiffable.

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray *_Nonnull toObjects;

    Swift

    var toObjects: [Any] { get }
  • The section controllers corresponding to the toObjects

    Declaration

    Objective-C

    @property (nonatomic, copy, readonly) NSArray<IGListSectionController *> *_Nonnull toSectionControllers;

    Swift

    var toSectionControllers: [IGListSectionController] { get }
================================================ FILE: docs/Classes.html ================================================ Classes Reference

Classes

The following classes are available globally.

  • An instance of IGListBatchUpdateData takes section indexes and item index paths and performs cleanup on init in order to perform a crash-free update via -[UICollectionView performBatchUpdates:completion:].

    See more

    Declaration

    Objective-C

    
    @interface IGListBatchUpdateData : NSObject

    Swift

    class ListBatchUpdateData : NSObject
  • A result object returned when diffing with sections.

    See more

    Declaration

    Objective-C

    
    @interface IGListIndexPathResult : NSObject

    Swift

    class ListIndexPathResult : NSObject
  • A result object returned when diffing with indexes.

    See more

    Declaration

    Objective-C

    
    @interface IGListIndexSetResult : NSObject

    Swift

    class ListIndexSetResult : NSObject
  • An object representing a move between indexes.

    See more

    Declaration

    Objective-C

    
    @interface IGListMoveIndex : NSObject

    Swift

    class ListMoveIndex : NSObject
  • An object representing a move between indexes.

    See more

    Declaration

    Objective-C

    
    @interface IGListMoveIndexPath : NSObject

    Swift

    class ListMoveIndexPath : NSObject
  • IGListAdapter objects provide an abstraction for feeds of objects in a UICollectionView by breaking each object into individual sections, called “section controllers”. These controllers (objects subclassing to IGListSectionController) act as a data source and delegate for each section.

    Feed implementations must act as the data source for an IGListAdapter in order to drive the objects and section controllers in a collection view.

    See more

    Declaration

    Objective-C

    
    @interface IGListAdapter : NSObject

    Swift

    @MainActor class ListAdapter : NSObject
  • Undocumented

    See more

    Declaration

    Objective-C

    @interface IGListAdapterDelegateAnnouncer : NSObject
    
    /// Default announcer for all `IGListAdapter`
    + (instancetype)sharedInstance;
    
    /// Add a delegate that will receive callbacks for all `IGListAdapter`.
    /// This is a weak reference, so you don't need to remove it on dealloc.
    - (void)addListener:(id<IGListAdapterDelegate>)listener;
    
    /// Remove delegate
    - (void)removeListener:(id<IGListAdapterDelegate>)listener;
    
    @end

    Swift

    class IGListAdapterDelegateAnnouncer : NSObject
  • An IGListAdapterUpdater is a concrete type that conforms to IGListUpdatingDelegate. It is an out-of-box updater for IGListAdapter objects to use.

    Note

    This updater performs re-entrant, coalesced updating for a list. It also uses a least-minimal diff for calculating UI updates when IGListAdapter calls -performUpdateWithCollectionView:fromObjects:toObjects:completion:.
    See more

    Declaration

    Objective-C

    
    @interface IGListAdapterUpdater : NSObject <IGListUpdatingDelegate>

    Swift

    @MainActor class ListAdapterUpdater : NSObject, ListUpdatingDelegate
  • This section controller uses a data source to transform its “top level” object into an array of diffable view models. It then automatically binds each view model to cells via the IGListBindable protocol.

    Models used with IGListBindingSectionController should take special care to always return YES for identical objects. That is, any objects with matching -diffIdentifiers should always be equal, that way the section controller can create new view models via the data source, create a diff, and update the specific cells that have changed.

    In Objective-C, your -isEqualToDiffableObject: can simply be:

    - (BOOL)isEqualToDiffableObject:(id)object {
      return YES;
    }
    

    In Swift:

    func isEqual(toDiffableObject object: IGListDiffable?) -> Bool {
      return true
    }
    

    Only when -diffIdentifiers match is object equality compared, so you can assume the class is the same, and the instance has already been checked.

    See more

    Declaration

    Objective-C

    
    @interface IGListBindingSectionController<
        __covariant ObjectType : id <IGListDiffable>> : IGListSectionController

    Swift

    @MainActor class ListBindingSectionController<ObjectType> : ListSectionController where ObjectType : ListDiffable
  • Special section controller that only contains a single item, and it will apply the view model update during -didUpdateObject: call, usually happened inside -[UICollectionView performBatchUpdates:completion:].

    This class is intended to be subclassed.

    See more

    Declaration

    Objective-C

    
    @interface IGListBindingSingleSectionController<
        __covariant ViewModel : id <IGListDiffable>, Cell : UICollectionViewCell *>
        : IGListSectionController

    Swift

    class ListBindingSingleSectionController<ViewModel, Cell> : ListSectionController where ViewModel : ListDiffable, Cell : UICollectionViewCell
  • This UICollectionView subclass allows for partial layout invalidation using IGListCollectionViewLayout, or custom layout classes that conform to IGListCollectionViewLayoutCompatible.

    Note

    When updating a collection view (ex: calling -insertSections), -invalidateLayoutWithContext gets called on the layout object. However, the invalidation context doesn’t provide details on which index paths are being modified, which typically forces a full layout re-calculation. IGListCollectionView gives IGListCollectionViewLayout the missing information to re-calculate only the modified layout attributes.
    See more

    Declaration

    Objective-C

    
    @interface IGListCollectionView : UICollectionView

    Swift

    class ListCollectionView : UICollectionView
  • This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, care must be taken not to stall the main thread in sizing delegate methods.

    This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that:

    • Your UICollectionView data source must also conform to UICollectionViewDelegateFlowLayout
    • Header support given via UICollectionElementKindSectionHeader

    All UICollectionViewDelegateFlowLayout methods are required and used by this layout:

    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
    - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;
    - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section;
    - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section;
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section;
    

    In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position of an item extends beyond the width of the collection view. When that happens, the item is “newlined” to the next row. The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row.

    Ex. of a section (2,0) with a large width causing a newline.

    |[ 0,0 ][ 1,0 ]         |
    |[         2,0         ]|
    

    A section with a non-zero height header will always cause that section to newline. Headers are always stretched to the width of the collection view, pinched with the section insets.

    Ex. of a section (2,0) with a header inset on the left/right.

    |[ 0,0 ][ 1,0 ]         |
    | >======header=======< |
    | [ 2,0 ]               |
    

    Section insets apply to items in the section no matter if they begin on a new row or are on the same row as a previous section.

    Ex. of a section (2) with multiple items and a left inset.

    |[ 0,0 ][ 1,0 ] >[ 2,0 ]|
    | >[ 2,1 ][ 2,2 ][ 2,3 ]|
    

    Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same section.

    In a horizontally scrolling layout, sections and items are flowed vertically until they need to be “newlined” to the next column. Headers, if used, are stretched to the height of the collection view, minus the section insets.

    Please see the unit tests for more configuration examples and expected output.

    See more

    Declaration

    Objective-C

    
    @interface IGListCollectionViewLayout
        : UICollectionViewLayout <IGListCollectionViewLayoutCompatible>

    Swift

    class ListCollectionViewLayout : UICollectionViewLayout, ListCollectionViewLayoutCompatible
  • The default invalidation context class used by IGListCollectionViewLayout.

    See more

    Declaration

    Objective-C

    
    @interface IGListCollectionViewLayoutInvalidationContext
        : UICollectionViewLayoutInvalidationContext

    Swift

    class ListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext
  • This class adds a helper layer to IGListSectionController to automatically store a generic object in didUpdateToObject:.

    See more

    Declaration

    Objective-C

    
    @interface IGListGenericSectionController<__covariant ObjectType>
        : IGListSectionController

    Swift

    class ListGenericSectionController<ObjectType> : ListSectionController where ObjectType : AnyObject
  • An IGListReloadDataUpdater is a concrete type that conforms to IGListUpdatingDelegate. It is an out-of-box updater for IGListAdapter objects to use.

    Note

    This updater performs simple, synchronous updates using -[UICollectionView reloadData].

    Declaration

    Objective-C

    
    @interface IGListReloadDataUpdater : NSObject <IGListUpdatingDelegate>

    Swift

    class ListReloadDataUpdater : NSObject, ListUpdatingDelegate
  • The base class for section controllers used in a list. This class is intended to be subclassed.

    See more

    Declaration

    Objective-C

    
    @interface IGListSectionController : NSObject

    Swift

    @MainActor class ListSectionController : NSObject
  • This section controller is meant to make building simple, single-cell lists easier. By providing the type of cell, a block to configure the cell, and a block to return the size of a cell, you can use an IGListAdapter-powered list with a simpler architecture.

    See more

    Declaration

    Objective-C

    
    @interface IGListSingleSectionController : IGListSectionController

    Swift

    class ListSingleSectionController : ListSectionController
  • Container object that holds the data needed for an update.

    See more

    Declaration

    Objective-C

    
    @interface IGListTransitionData : NSObject

    Swift

    class ListTransitionData : NSObject
================================================ FILE: docs/Constants.html ================================================ Constants Reference

Constants

The following constants are available globally.

  • Project version number for IGListKit.

    Declaration

    Objective-C

    extern double IGListKitVersionNumber

    Swift

    var IGListKitVersionNumber: Double
  • Project version string for IGListKit.

    Declaration

    Objective-C

    extern const unsigned char IGListKitVersionString[]

    Swift

    let IGListKitVersionString: <<error type>>
================================================ FILE: docs/Enums/IGListAdapterUpdateType.html ================================================ IGListAdapterUpdateType Enumeration Reference

IGListAdapterUpdateType

Objective-C

enum IGListAdapterUpdateType : NSInteger {}

Swift

enum IGListAdapterUpdateType : Int, @unchecked Sendable

The type of update that was performed by an IGListAdapter.

================================================ FILE: docs/Enums/IGListDiffOption.html ================================================ IGListDiffOption Enumeration Reference

IGListDiffOption

Objective-C

enum IGListDiffOption : NSInteger {}

Swift

enum IGListDiffOption : Int, @unchecked Sendable

An option for how to do comparisons between similar objects.

================================================ FILE: docs/Enums/IGListExperiment.html ================================================ IGListExperiment Enumeration Reference

IGListExperiment

Objective-C

enum IGListExperiment : NSInteger {}

Swift

struct IGListExperiment : OptionSet, @unchecked Sendable

Bitmask-able options used for pre-release feature testing.

  • Specifies no experiments.

    Declaration

    Objective-C

    IGListExperimentNone = 1 << 1

    Swift

    static var none: IGListExperiment { get }
  • Test invalidating layout when cell reloads/updates in IGListBindingSectionController.

    Declaration

    Objective-C

    IGListExperimentInvalidateLayoutForUpdates = 1 << 2

    Swift

    static var invalidateLayoutForUpdates: IGListExperiment { get }
  • Throw NSInternalInconsistencyException during an update

    Declaration

    Objective-C

    IGListExperimentThrowOnInconsistencyException = 1 << 3

    Swift

    static var throwOnInconsistencyException: IGListExperiment { get }
  • Remove the early exit so multiple updates can’t happen at once

    Declaration

    Objective-C

    IGListExperimentRemoveDataSourceChangeEarlyExit = 1 << 4

    Swift

    static var removeDataSourceChangeEarlyExit: IGListExperiment { get }
  • Avoids creating off-screen cells

    Declaration

    Objective-C

    IGListExperimentFixPreferredFocusedView = 1 << 5

    Swift

    static var fixPreferredFocusedView: IGListExperiment { get }
================================================ FILE: docs/Enums.html ================================================ Enumerations Reference

Enumerations

The following enumerations are available globally.

  • An option for how to do comparisons between similar objects.

    See more

    Declaration

    Objective-C

    enum IGListDiffOption : NSInteger {}

    Swift

    enum IGListDiffOption : Int, @unchecked Sendable
  • Bitmask-able options used for pre-release feature testing.

    See more

    Declaration

    Objective-C

    enum IGListExperiment : NSInteger {}

    Swift

    struct IGListExperiment : OptionSet, @unchecked Sendable
  • The type of update that was performed by an IGListAdapter.

    See more

    Declaration

    Objective-C

    enum IGListAdapterUpdateType : NSInteger {}

    Swift

    enum IGListAdapterUpdateType : Int, @unchecked Sendable
================================================ FILE: docs/Functions.html ================================================ Functions Reference

Functions

The following functions are available globally.

  • Creates a diff using indexes between two collections.

    Declaration

    Objective-C

    extern IGListIndexSetResult *_Nonnull IGListDiff(
        NSArray<id<IGListDiffable>> *_Nullable oldArray,
        NSArray<id<IGListDiffable>> *_Nullable newArray, IGListDiffOption option)

    Swift

    func ListDiff(oldArray: [any ListDiffable]?, newArray: [any ListDiffable]?, option: IGListDiffOption) -> ListIndexSetResult

    Parameters

    oldArray

    The old objects to diff against.

    newArray

    The new objects.

    option

    An option on how to compare objects.

    Return Value

    A result object containing affected indexes.

  • Creates a diff using index paths between two collections.

    Declaration

    Objective-C

    extern IGListIndexPathResult *_Nonnull IGListDiffPaths(
        NSInteger fromSection, NSInteger toSection,
        NSArray<id<IGListDiffable>> *_Nullable oldArray,
        NSArray<id<IGListDiffable>> *_Nullable newArray, IGListDiffOption option)

    Swift

    func ListDiffPaths(fromSection: Int, toSection: Int, oldArray: [any ListDiffable]?, newArray: [any ListDiffable]?, option: IGListDiffOption) -> ListIndexPathResult

    Parameters

    fromSection

    The old section.

    toSection

    The new section.

    oldArray

    The old objects to diff against.

    newArray

    The new objects.

    option

    An option on how to compare objects.

    Return Value

    A result object containing affected indexes.

  • Check if an experiment is enabled in a bitmask.

    Declaration

    Objective-C

    static inline BOOL IGListExperimentEnabled(IGListExperiment mask,
                                               IGListExperiment option)

    Swift

    func ListExperimentEnabled(mask: IGListExperiment, option: IGListExperiment) -> Bool

    Parameters

    mask

    The bitmask of experiments.

    option

    The option to compare with.

    Return Value

    YES if the option is in the bitmask, otherwise NO.

================================================ FILE: docs/Guides.html ================================================ Guides Reference
================================================ FILE: docs/Protocols/IGListAdapterDataSource.html ================================================ IGListAdapterDataSource Protocol Reference

IGListAdapterDataSource

Objective-C

@protocol IGListAdapterDataSource <NSObject>

Swift

@MainActor protocol ListAdapterDataSource : NSObjectProtocol

Implement this protocol to provide data to an IGListAdapter.

  • Asks the data source for the objects to display in the list.

    Declaration

    Objective-C

    - (nonnull NSArray<id<IGListDiffable>> *)objectsForListAdapter:
        (nonnull IGListAdapter *)listAdapter;

    Swift

    func objects(for listAdapter: IGListAdapter) -> [any ListDiffable]

    Parameters

    listAdapter

    The list adapter requesting this information.

    Return Value

    An array of objects for the list.

  • Asks the data source for a section controller for the specified object in the list.

    Note

    New section controllers should be initialized here for objects when asked. You may pass any other data to the section controller at this time.

    Section controllers are initialized for all objects whenever the IGListAdapter is created, updated, or reloaded. Section controllers are reused when objects are moved or updated. Maintaining the -[IGListDiffable diffIdentifier] guarantees this.

    Declaration

    Objective-C

    - (nonnull IGListSectionController *)listAdapter:
                                             (nonnull IGListAdapter *)listAdapter
                          sectionControllerForObject:(nonnull id)object;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, sectionControllerFor object: Any) -> IGListSectionController

    Parameters

    listAdapter

    The list adapter requesting this information.

    object

    An object in the list.

    Return Value

    A new section controller instance that can be displayed in the list.

  • Asks the data source for a view to use as the collection view background when the list is empty.

    Note

    This method is called every time the list adapter is updated. You are free to return new views every time, but for performance reasons you may want to retain the view and return it here. The infra is only responsible for adding the background view and maintaining its visibility.

    Declaration

    Objective-C

    - (nullable UIView *)emptyViewForListAdapter:
        (nonnull IGListAdapter *)listAdapter;

    Swift

    func emptyView(for listAdapter: IGListAdapter) -> UIView?

    Parameters

    listAdapter

    The list adapter requesting this information.

    Return Value

    A view to use as the collection view background, or nil if you don’t want a background view.

================================================ FILE: docs/Protocols/IGListAdapterDelegate.html ================================================ IGListAdapterDelegate Protocol Reference

IGListAdapterDelegate

Objective-C

@protocol IGListAdapterDelegate <NSObject>

Swift

@MainActor protocol ListAdapterDelegate : NSObjectProtocol

Conform to IGListAdapterDelegate to receive display events for objects in a list.

  • Notifies the delegate that a list object is about to be displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        willDisplayObject:(nonnull id)object
                  atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, willDisplay object: Any, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    object

    The object that will display.

    index

    The index of the object in the list.

  • Notifies the delegate that a list object is no longer being displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDisplayingObject:(nonnull id)object
                       atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didEndDisplaying object: Any, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    object

    The object that ended display.

    index

    The index of the object in the list.

  • Notifies the delegate that a list object is about to be displayed. This method is distinct from willDisplayObject:atIndex because this method gets called whenever a cell will be displayed on screen as opposed to willDisplayObject:atIndex which only gets called for once per section.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        willDisplayObject:(nonnull id)object
                     cell:(nonnull UICollectionViewCell *)cell
              atIndexPath:(nonnull NSIndexPath *)indexPath;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, willDisplay object: Any, cell: UICollectionViewCell, at indexPath: IndexPath)

    Parameters

    listAdapter

    The list adapter sending this information.

    object

    The object that will display.

    cell

    The cell which contains the object that will display.

    indexPath

    The index path of the object in the list.

  • Notifies the delegate that a list object is no longer being displayed. This method is distinct from didEndDisplayingObject:atIndex because this method gets called whenever a cell ends display on screen as opposed to didEndDisplayingObject:atIndex which only gets called once when the section fully ends display.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDisplayingObject:(nonnull id)object
                          cell:(nonnull UICollectionViewCell *)cell
                   atIndexPath:(nonnull NSIndexPath *)indexPath;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didEndDisplaying object: Any, cell: UICollectionViewCell, at indexPath: IndexPath)

    Parameters

    listAdapter

    The list adapter sending this information.

    object

    The object that ended display.

    cell

    The cell which contains the object that ended display.

    indexPath

    The index path of the object in the list.

================================================ FILE: docs/Protocols/IGListAdapterMoveDelegate.html ================================================ IGListAdapterMoveDelegate Protocol Reference

IGListAdapterMoveDelegate

Objective-C

@protocol IGListAdapterMoveDelegate <NSObject>

Swift

@MainActor protocol ListAdapterMoveDelegate : NSObjectProtocol

Conform to IGListAdapterMoveDelegate to receive interactive reordering requests.

  • Asks the delegate to move a section object as the result of interactive reordering.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
             moveObject:(nonnull id)object
                   from:(nonnull NSArray *)previousObjects
                     to:(nonnull NSArray *)objects;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, move object: Any, from previousObjects: [Any], to objects: [Any])

    Parameters

    listAdapter

    The list adapter sending this information.

    object

    the object that was moved

    previousObjects

    The array of objects prior to the move.

    objects

    The array of objects after the move.

================================================ FILE: docs/Protocols/IGListAdapterPerformanceDelegate.html ================================================ IGListAdapterPerformanceDelegate Protocol Reference

IGListAdapterPerformanceDelegate

Objective-C

@protocol IGListAdapterPerformanceDelegate <NSObject>

Swift

protocol ListAdapterPerformanceDelegate : NSObjectProtocol

IGListAdapterPerformanceDelegate can be used to measure cell dequeue, display, size, and scroll callbacks.

  • Will call -[IGListAdapter collectionView:cellForItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapterWillCallDequeueCell:(nonnull IGListAdapter *)listAdapter;

    Swift

    func listAdapterWillCallDequeueCell(_ listAdapter: IGListAdapter)

    Parameters

    listAdapter

    The list adapter sending this information.

  • Did finish calling -[IGListAdapter collectionView:cellForItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
         didCallDequeueCell:(nonnull UICollectionViewCell *)cell
        onSectionController:(nonnull IGListSectionController *)sectionController
                    atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didCallDequeue cell: UICollectionViewCell, on sectionController: IGListSectionController, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    cell

    A cell that was dequeued.

    sectionController

    The section controller providing the cell.

    index

    Item index of the cell.

  • Will call -[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapterWillCallDisplayCell:(nonnull IGListAdapter *)listAdapter;

    Swift

    func listAdapterWillCallDisplayCell(_ listAdapter: IGListAdapter)

    Parameters

    listAdapter

    The list adapter sending this information.

  • Did finish calling -[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:].

    Note

    Keep in mind this also includes calling the IGListAdapter‘s collectionViewDelegate and workingRangeHandler.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
         didCallDisplayCell:(nonnull UICollectionViewCell *)cell
        onSectionController:(nonnull IGListSectionController *)sectionController
                    atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didCallDisplay cell: UICollectionViewCell, on sectionController: IGListSectionController, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    cell

    A cell that will be displayed.

    sectionController

    The section controller for that cell.

    index

    Item index of the cell.

  • Will call -[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapterWillCallEndDisplayCell:(nonnull IGListAdapter *)listAdapter;

    Swift

    func listAdapterWillCallEndDisplayCell(_ listAdapter: IGListAdapter)

    Parameters

    listAdapter

    The list adapter sending this information.

  • Did finish calling -[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:].

    Note

    Keep in mind this also includes calling the IGListAdapter‘s collectionViewDelegate and workingRangeHandler.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didCallEndDisplayCell:(nonnull UICollectionViewCell *)cell
          onSectionController:(nonnull IGListSectionController *)sectionController
                      atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didCallEndDisplay cell: UICollectionViewCell, on sectionController: IGListSectionController, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    cell

    A cell that was displayed.

    sectionController

    The section controller for that cell.

    index

    Item index of the cell.

  • Will call -[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapterWillCallSize:(nonnull IGListAdapter *)listAdapter;

    Swift

    func listAdapterWillCallSize(_ listAdapter: IGListAdapter)

    Parameters

    listAdapter

    The list adapter sending this information.

  • Did finish calling -[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:].

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didCallSizeOnSectionController:
            (nonnull IGListSectionController *)sectionController
                               atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didCallSizeOn sectionController: IGListSectionController, at index: Int)

    Parameters

    listAdapter

    The list adapter sending this information.

    sectionController

    The section controller providing the size.

    index

    Item index used to calculate the size.

  • Will call -[IGListAdapter scrollViewDidScroll:].

    Declaration

    Objective-C

    - (void)listAdapterWillCallScroll:(nonnull IGListAdapter *)listAdapter;

    Swift

    func listAdapterWillCallScroll(_ listAdapter: IGListAdapter)

    Parameters

    listAdapter

    The list adapter sending this information.

  • Did finish calling -[IGListAdapter scrollViewDidScroll:].

    Note

    Keep in mind this also includes calling the IGListAdapter‘s scrollViewDelegate and all visible IGListSectioControllers.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
          didCallScroll:(nonnull UIScrollView *)scrollView;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didCallScroll scrollView: UIScrollView)

    Parameters

    listAdapter

    The list adapter sending this information.

    scrollView

    The scroll view backing the UICollectionView.

================================================ FILE: docs/Protocols/IGListAdapterUpdateListener.html ================================================ IGListAdapterUpdateListener Protocol Reference

IGListAdapterUpdateListener

Objective-C

@protocol IGListAdapterUpdateListener <NSObject>

Swift

@MainActor protocol ListAdapterUpdateListener : NSObjectProtocol

Conform to this protocol to receive events about IGListAdapter updates.

================================================ FILE: docs/Protocols/IGListAdapterUpdaterDelegate.html ================================================ IGListAdapterUpdaterDelegate Protocol Reference

IGListAdapterUpdaterDelegate

Objective-C

@protocol IGListAdapterUpdaterDelegate <NSObject>

Swift

protocol ListAdapterUpdaterDelegate : NSObjectProtocol

A protocol that receives events about IGListAdapterUpdater operations.

  • Notifies the delegate that the updater is about to beging diffing.

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
           willDiffFromObjects:(nullable NSArray<id<IGListDiffable>> *)fromObjects
                     toObjects:(nullable NSArray<id<IGListDiffable>> *)toObjects;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willDiffFromObjects fromObjects: [any IGListDiffable]?, toObjects: [any IGListDiffable]?)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    fromObjects

    The items transitioned from in the batch updates, if any.

    toObjects

    The items transitioned to in the batch updates, if any.

  • Notifies the delegate that the updater finished diffing.

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
            didDiffWithResults:(nullable IGListIndexSetResult *)listIndexSetResults
            onBackgroundThread:(BOOL)onBackgroundThread;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, didDiffWithResults listIndexSetResults: IGListIndexSetResult?, onBackgroundThread: Bool)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    listIndexSetResults

    The diffing result of indices to be inserted/removed/updated/moved/etc.

    onBackgroundThread

    Was the diffing performed on a background thread

  • Notifies the delegate that the updater will call -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        willPerformBatchUpdatesWithCollectionView:
            (nonnull UICollectionView *)collectionView
                                      fromObjects:
                                          (nullable NSArray<id<IGListDiffable>> *)
                                              fromObjects
                                        toObjects:
                                            (nullable NSArray<id<IGListDiffable>> *)
                                                toObjects
                               listIndexSetResult:(nullable IGListIndexSetResult *)
                                                      listIndexSetResults
                                         animated:(BOOL)animated;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willPerformBatchUpdatesWith collectionView: UICollectionView, fromObjects: [any IGListDiffable]?, toObjects: [any IGListDiffable]?, listIndexSetResult listIndexSetResults: IGListIndexSetResult?, animated: Bool)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    collectionView

    The collection view that will perform the batch updates.

    fromObjects

    The items transitioned from in the batch updates, if any.

    toObjects

    The items transitioned to in the batch updates, if any.

    listIndexSetResults

    The diffing result of indices to be inserted/removed/updated/moved/etc.

    animated

    Is the cell transtion animated

  • Notifies the delegate that the updater successfully finished -[UICollectionView performBatchUpdates:completion:].

    Note

    This event is called in the completion block of the batch update.

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        didPerformBatchUpdates:(nonnull IGListBatchUpdateData *)updates
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, didPerformBatchUpdates updates: ListBatchUpdateData, collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    updates

    The batch updates that were applied to the collection view.

    collectionView

    The collection view that performed the batch updates.

  • Notifies the delegate that the updater will call -[UICollectionView insertItemsAtIndexPaths:].

    Note

    This event is only sent when outside of -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
          willInsertIndexPaths:(nonnull NSArray<NSIndexPath *> *)indexPaths
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willInsert indexPaths: [IndexPath], collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    indexPaths

    An array of index paths that will be inserted.

    collectionView

    The collection view that will perform the insert.

  • Notifies the delegate that the updater will call -[UICollectionView deleteItemsAtIndexPaths:].

    Note

    This event is only sent when outside of -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
          willDeleteIndexPaths:(nonnull NSArray<NSIndexPath *> *)indexPaths
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willDelete indexPaths: [IndexPath], collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    indexPaths

    An array of index paths that will be deleted.

    collectionView

    The collection view that will perform the delete.

  • Notifies the delegate that the updater will call -[UICollectionView moveItemAtIndexPath:toIndexPath:]

    Note

    This event is only sent when outside of -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
         willMoveFromIndexPath:(nonnull NSIndexPath *)fromIndexPath
                   toIndexPath:(nonnull NSIndexPath *)toIndexPath
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willMoveFrom fromIndexPath: IndexPath, to toIndexPath: IndexPath, collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    fromIndexPath

    The index path of the item that will be moved.

    toIndexPath

    The index path to move the item to.

    collectionView

    The collection view that will perform the move.

  • Notifies the delegate that the updater will call -[UICollectionView reloadItemsAtIndexPaths:].

    Note

    This event is only sent when outside of -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
          willReloadIndexPaths:(nonnull NSArray<NSIndexPath *> *)indexPaths
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willReload indexPaths: [IndexPath], collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    indexPaths

    An array of index paths that will be reloaded.

    collectionView

    The collection view that will perform the reload.

  • Notifies the delegate that the updater will call -[UICollectionView reloadSections:].

    Note

    This event is only sent when outside of -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
            willReloadSections:(nonnull NSIndexSet *)sections
                collectionView:(nonnull UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willReloadSections sections: IndexSet, collectionView: UICollectionView)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    sections

    The sections that will be reloaded

    collectionView

    The collection view that will perform the reload.

  • Notifies the delegate that the updater will call -[UICollectionView reloadData].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        willReloadDataWithCollectionView:(nonnull UICollectionView *)collectionView
                        isFallbackReload:(BOOL)isFallbackReload;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willReloadDataWith collectionView: UICollectionView, isFallbackReload: Bool)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    collectionView

    The collection view that will be reloaded.

    isFallbackReload

    The reload was a fallback because we could not performBatchUpdate

  • Notifies the delegate that the updater successfully called -[UICollectionView reloadData].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        didReloadDataWithCollectionView:(nonnull UICollectionView *)collectionView
                       isFallbackReload:(BOOL)isFallbackReload;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, didReloadDataWith collectionView: UICollectionView, isFallbackReload: Bool)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    collectionView

    The collection view that reloaded.

    isFallbackReload

    The reload was a fallback because we could not performBatchUpdate

  • Notifies the delegate that the collection view threw an exception in -[UICollectionView performBatchUpdates:completion:].

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
                collectionView:(nonnull UICollectionView *)collectionView
        willCrashWithException:(nonnull NSException *)exception
                   fromObjects:(nullable NSArray *)fromObjects
                     toObjects:(nullable NSArray *)toObjects
                    diffResult:(nonnull IGListIndexSetResult *)diffResult
                       updates:(nonnull IGListBatchUpdateData *)updates;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, collectionView: UICollectionView, willCrashWith exception: NSException, from fromObjects: [Any]?, to toObjects: [Any]?, diffResult: IGListIndexSetResult, updates: ListBatchUpdateData)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    collectionView

    The collection view being updated.

    exception

    The exception thrown by the collection view.

    fromObjects

    The items transitioned from in the diff, if any.

    toObjects

    The items transitioned to in the diff, if any.

    diffResult

    The diff result that were computed from fromObjects and toObjects.

    updates

    The batch updates that were applied to the collection view.

  • Notifies the delegate that the updater detected an imminent crash, such as when a section controller returns a nil cell. This provides an opportunity to log diagnostic information before the crash occurs.

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        willCrashWithCollectionView:(nonnull UICollectionView *)collectionView
             sectionControllerClass:(nullable Class)sectionControllerClass;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, willCrashWith collectionView: UICollectionView, sectionControllerClass: AnyClass?)

    Parameters

    listAdapterUpdater

    The adapter updater that detected the issue.

    collectionView

    The collection view involved in the crash.

    sectionControllerClass

    The class of the section controller that caused the issue, if available.

  • Notifies the delegate that the updater finished without performing any batch updates or reloads

    Declaration

    Objective-C

    - (void)listAdapterUpdater:(nonnull IGListAdapterUpdater *)listAdapterUpdater
        didFinishWithoutUpdatesWithCollectionView:
            (nullable UICollectionView *)collectionView;

    Swift

    func listAdapterUpdater(_ listAdapterUpdater: IGListAdapterUpdater, didFinishWithoutUpdatesWith collectionView: UICollectionView?)

    Parameters

    listAdapterUpdater

    The adapter updater owning the transition.

    collectionView

    The collection view that reloaded.

================================================ FILE: docs/Protocols/IGListBatchContext.html ================================================ IGListBatchContext Protocol Reference

IGListBatchContext

Objective-C

@protocol IGListBatchContext <NSObject>

Swift

protocol ListBatchContext : NSObjectProtocol

Objects conforming to the IGListBatchContext protocol provide a way for section controllers to mutate their cells or reload everything within the section.

  • Reloads cells in the section controller.

    Declaration

    Objective-C

    - (void)reloadInSectionController:
                (nonnull IGListSectionController *)sectionController
                            atIndexes:(nonnull NSIndexSet *)indexes;

    Swift

    func reload(in sectionController: IGListSectionController, at indexes: IndexSet)

    Parameters

    sectionController

    The section controller who’s cells need reloading.

    indexes

    The indexes of items that need reloading.

  • Inserts cells in the list.

    Declaration

    Objective-C

    - (void)insertInSectionController:
                (nonnull IGListSectionController *)sectionController
                            atIndexes:(nonnull NSIndexSet *)indexes;

    Swift

    func insert(in sectionController: IGListSectionController, at indexes: IndexSet)

    Parameters

    sectionController

    The section controller who’s cells need inserting.

    indexes

    The indexes of items that need inserting.

  • Deletes cells in the list.

    Declaration

    Objective-C

    - (void)deleteInSectionController:
                (nonnull IGListSectionController *)sectionController
                            atIndexes:(nonnull NSIndexSet *)indexes;

    Swift

    func delete(in sectionController: IGListSectionController, at indexes: IndexSet)

    Parameters

    sectionController

    The section controller who’s cells need deleted.

    indexes

    The indexes of items that need deleting.

  • Invalidates layouts of cells at specific in the section controller.

    Declaration

    Objective-C

    - (void)invalidateLayoutInSectionController:
                (nonnull IGListSectionController *)sectionController
                                      atIndexes:(nonnull NSIndexSet *)indexes;

    Swift

    func invalidateLayout(in sectionController: IGListSectionController, at indexes: IndexSet)

    Parameters

    sectionController

    The section controller who’s cells need invalidating.

    indexes

    The indexes of items that need invalidating.

  • Moves a cell from one index to another within the section controller.

    Declaration

    Objective-C

    - (void)moveInSectionController:
                (nonnull IGListSectionController *)sectionController
                          fromIndex:(NSInteger)fromIndex
                            toIndex:(NSInteger)toIndex;

    Swift

    func move(in sectionController: IGListSectionController, from fromIndex: Int, to toIndex: Int)

    Parameters

    sectionController

    The section controller who’s cell needs moved.

    fromIndex

    The index the cell is currently in.

    toIndex

    The index the cell should move to.

  • Reloads the entire section controller.

    Declaration

    Objective-C

    - (void)reloadSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func reload(_ sectionController: IGListSectionController)

    Parameters

    sectionController

    The section controller who’s cells need reloading.

  • Moves a section controller from one index to another during interactive reordering.

    Declaration

    Objective-C

    - (void)moveSectionControllerInteractive:
                (nonnull IGListSectionController *)sectionController
                                   fromIndex:(NSInteger)fromIndex
                                     toIndex:(NSInteger)toIndex;

    Swift

    func moveSectionControllerInteractive(_ sectionController: IGListSectionController, from fromIndex: Int, to toIndex: Int)

    Parameters

    sectionController

    The section controller to move.

    fromIndex

    The index where the section currently resides.

    toIndex

    The index the section should move to.

  • Moves an object within a section controller from one index to another during interactive reordering.

    Declaration

    Objective-C

    - (void)moveInSectionControllerInteractive:
                (nonnull IGListSectionController *)sectionController
                                     fromIndex:(NSInteger)fromIndex
                                       toIndex:(NSInteger)toIndex;

    Swift

    func move(inSectionControllerInteractive sectionController: IGListSectionController, from fromIndex: Int, to toIndex: Int)

    Parameters

    sectionController

    The section controller containing the object to move.

    fromIndex

    The index where the object currently resides.

    toIndex

    The index the object should move to.

  • Reverts an move from one indexPath to another during interactive reordering.

    Declaration

    Objective-C

    - (void)revertInvalidInteractiveMoveFromIndexPath:
                (nonnull NSIndexPath *)sourceIndexPath
                                          toIndexPath:(nonnull NSIndexPath *)
                                                          destinationIndexPath;

    Swift

    func revertInvalidInteractiveMove(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)

    Parameters

    sourceIndexPath

    The indexPath the item was originally in.

    destinationIndexPath

    The indexPath the item was moving to.

================================================ FILE: docs/Protocols/IGListBindable.html ================================================ IGListBindable Protocol Reference

IGListBindable

Objective-C

@protocol IGListBindable <NSObject>

Swift

@MainActor protocol ListBindable : NSObjectProtocol

A protocol for cells that configure themselves given a view model.

  • Tells the cell to configure itself with the given view model.

    Note

    The view model can change many times throughout the lifetime of a cell as the model values change and the cell is reused. Implementations should use only this method to do their configuration.

    Declaration

    Objective-C

    - (void)bindViewModel:(nonnull id)viewModel;

    Swift

    func bindViewModel(_ viewModel: Any)

    Parameters

    viewModel

    The view model for the cell.

================================================ FILE: docs/Protocols/IGListBindingSectionControllerDataSource.html ================================================ IGListBindingSectionControllerDataSource Protocol Reference

IGListBindingSectionControllerDataSource

Objective-C

@protocol IGListBindingSectionControllerDataSource <NSObject>

Swift

@MainActor protocol ListBindingSectionControllerDataSource : NSObjectProtocol

A protocol that returns data to power cells in an IGListBindingSectionController.

  • Create an array of view models given a top-level object.

    Declaration

    Objective-C

    - (nonnull NSArray<id<IGListDiffable>> *)
          sectionController:
              (nonnull IGListBindingSectionController *)sectionController
        viewModelsForObject:(nonnull id)object;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, viewModelsFor object: Any) -> [any IGListDiffable]

    Parameters

    sectionController

    The section controller requesting view models.

    object

    The top-level object that powers the section controller.

    Return Value

    A new array of view models.

  • Return a dequeued cell for a given view model.

    Note

    The section controller will call -bindViewModel: with the provided view model after the cell is dequeued. You should handle cell configuration using this method. However, you can do additional configuration at this stage as well.

    Declaration

    Objective-C

    - (nonnull UICollectionViewCell<IGListBindable> *)
        sectionController:
            (nonnull IGListBindingSectionController *)sectionController
         cellForViewModel:(nonnull id)viewModel
                  atIndex:(NSInteger)index;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, cellForViewModel viewModel: Any, at index: Int) -> any UICollectionViewCell & IGListBindable

    Parameters

    sectionController

    The section controller requesting a cell.

    viewModel

    The view model for the cell.

    index

    The index of the view model.

    Return Value

    A dequeued cell.

  • Return a cell size for a given view model.

    Declaration

    Objective-C

    - (CGSize)sectionController:
                  (nonnull IGListBindingSectionController *)sectionController
               sizeForViewModel:(nonnull id)viewModel
                        atIndex:(NSInteger)index;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, sizeForViewModel viewModel: Any, at index: Int) -> CGSize

    Parameters

    sectionController

    The section controller requesting a size.

    viewModel

    The view model for the cell.

    index

    The index of the view model.

    Return Value

    A size for the view model.

================================================ FILE: docs/Protocols/IGListBindingSectionControllerSelectionDelegate.html ================================================ IGListBindingSectionControllerSelectionDelegate Protocol Reference

IGListBindingSectionControllerSelectionDelegate

Objective-C

@protocol IGListBindingSectionControllerSelectionDelegate <NSObject>

Swift

@MainActor protocol ListBindingSectionControllerSelectionDelegate : NSObjectProtocol

A protocol that handles cell selection events in an IGListBindingSectionController.

  • Tells the delegate that a cell at a given index was selected.

    Declaration

    Objective-C

    - (void)sectionController:
                (nonnull IGListBindingSectionController *)sectionController
         didSelectItemAtIndex:(NSInteger)index
                    viewModel:(nonnull id)viewModel;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, didSelectItemAt index: Int, viewModel: Any)

    Parameters

    sectionController

    The section controller the selection occurred in.

    index

    The index of the selected cell.

    viewModel

    The view model that was bound to the cell.

  • Tells the delegate that a cell at a given index was deselected.

    Declaration

    Objective-C

    - (void)sectionController:
                (nonnull IGListBindingSectionController *)sectionController
        didDeselectItemAtIndex:(NSInteger)index
                     viewModel:(nonnull id)viewModel;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, didDeselectItemAt index: Int, viewModel: Any)

    Parameters

    sectionController

    The section controller the deselection occurred in.

    index

    The index of the deselected cell.

    viewModel

    The view model that was bound to the cell.

  • Tells the delegate that a cell at a given index was highlighted.

    Declaration

    Objective-C

    - (void)sectionController:
                (nonnull IGListBindingSectionController *)sectionController
        didHighlightItemAtIndex:(NSInteger)index
                      viewModel:(nonnull id)viewModel;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, didHighlightItemAt index: Int, viewModel: Any)

    Parameters

    sectionController

    The section controller the highlight occurred in.

    index

    The index of the highlighted cell.

    viewModel

    The view model that was bound to the cell.

  • Tells the delegate that a cell at a given index was unhighlighted.

    Declaration

    Objective-C

    - (void)sectionController:
                (nonnull IGListBindingSectionController *)sectionController
        didUnhighlightItemAtIndex:(NSInteger)index
                        viewModel:(nonnull id)viewModel;

    Swift

    func sectionController(_ sectionController: IGListBindingSectionController, didUnhighlightItemAt index: Int, viewModel: Any)

    Parameters

    sectionController

    The section controller the unhighlight occurred in.

    index

    The index of the unhighlighted cell.

    viewModel

    The view model that was bound to the cell.

  • Tells the delegate that a cell has requested a menu configuration.

    Declaration

    Objective-C

    - (UIContextMenuConfiguration *_Nullable)
                             sectionController:
                                 (nonnull IGListBindingSectionController *)
                                     sectionController
        contextMenuConfigurationForItemAtIndex:(NSInteger)index
                                         point:(CGPoint)point
                                     viewModel:(nonnull id)viewModel;

    Swift

    optional func sectionController(_ sectionController: IGListBindingSectionController, contextMenuConfigurationForItemAt index: Int, point: CGPoint, viewModel: Any) -> UIContextMenuConfiguration?

    Parameters

    sectionController

    The section controller the request of a menu configuration occurred in.

    index

    The index of the cell that is being longed tap.

    point

    The point of the tap on the cell.

    viewModel

    The view model that was bound to the cell.

    Return Value

    An object that conforms to UIContextMenuConfiguration.

================================================ FILE: docs/Protocols/IGListCollectionContext.html ================================================ IGListCollectionContext Protocol Reference

IGListCollectionContext

Objective-C

@protocol IGListCollectionContext <NSObject>

Swift

protocol ListCollectionContext : NSObjectProtocol

The collection context provides limited access to the collection-related information that section controllers need for operations like sizing, dequeuing cells, inserting, deleting, reloading, etc.

  • The size of the collection view. You can use this for sizing cells.

    Declaration

    Objective-C

    @property (nonatomic, readonly) CGSize containerSize;

    Swift

    var containerSize: CGSize { get }
  • The content insets of the collection view. You can use this for sizing cells.

    Declaration

    Objective-C

    @property (nonatomic, readonly) UIEdgeInsets containerInset;

    Swift

    var containerInset: UIEdgeInsets { get }
  • The adjusted content insets of the collection view. Equivalent to containerInset under iOS 11.

    Declaration

    Objective-C

    @property (nonatomic, readonly) UIEdgeInsets adjustedContainerInset;

    Swift

    var adjustedContainerInset: UIEdgeInsets { get }
  • The size of the collection view with content insets applied.

    Declaration

    Objective-C

    @property (nonatomic, readonly) CGSize insetContainerSize;

    Swift

    var insetContainerSize: CGSize { get }
  • The content offset of the collection view.

    Declaration

    Objective-C

    @property (nonatomic, readonly) CGPoint containerContentOffset;

    Swift

    var containerContentOffset: CGPoint { get }
  • The trait collection of the collection view.

    Declaration

    Objective-C

    @property (nonatomic, readonly, nullable) UITraitCollection *traitCollection;

    Swift

    var traitCollection: UITraitCollection? { get }
  • The current scrolling traits of the underlying collection view.

    Declaration

    Objective-C

    @property (nonatomic, readonly) IGListCollectionScrollingTraits scrollingTraits;

    Swift

    var scrollingTraits: IGListCollectionScrollingTraits { get }
  • A bitmask of experiments to conduct on the section controller.

    Declaration

    Objective-C

    @property (nonatomic) IGListExperiment experiments;

    Swift

    var experiments: IGListExperiment { get set }
  • Returns size of the collection view relative to the section controller.

    Declaration

    Objective-C

    - (CGSize)containerSizeForSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func containerSize(for sectionController: IGListSectionController) -> CGSize

    Parameters

    sectionController

    The section controller requesting this information.

    Return Value

    The size of the collection view minus the given section controller’s insets.

  • Returns the index of the specified cell in the collection relative to the section controller.

    Declaration

    Objective-C

    - (NSInteger)indexForCell:(nonnull UICollectionViewCell *)cell
            sectionController:(nonnull IGListSectionController *)sectionController;

    Swift

    func index(for cell: UICollectionViewCell, sectionController: IGListSectionController) -> Int

    Parameters

    cell

    An existing cell in the collection.

    sectionController

    The section controller requesting this information.

    Return Value

    The index of the cell or NSNotFound if it does not exist in the collection.

  • Returns the cell in the collection at the specified index for the section controller.

    Warning

    This method may return nil if the cell is offscreen.

    Declaration

    Objective-C

    - (nullable __kindof UICollectionViewCell *)
        cellForItemAtIndex:(NSInteger)index
         sectionController:(nonnull IGListSectionController *)sectionController;

    Swift

    func cellForItem(at index: Int, sectionController: IGListSectionController) -> UICollectionViewCell?

    Parameters

    index

    The index of the desired cell.

    sectionController

    The section controller requesting this information.

    Return Value

    The collection view cell, or nil if not found.

  • Returns the supplementary view in the collection at the specified index for the section controller.

    Warning

    This method may return nil if the cell is offscreen.

    Declaration

    Objective-C

    - (nullable __kindof UICollectionReusableView *)
        viewForSupplementaryElementOfKind:(nonnull NSString *)elementKind
                                  atIndex:(NSInteger)index
                        sectionController:
                            (nonnull IGListSectionController *)sectionController;

    Swift

    func viewForSupplementaryElement(ofKind elementKind: String, at index: Int, sectionController: IGListSectionController) -> UICollectionReusableView?

    Parameters

    elementKind

    The element kind of the supplementary view.

    index

    The index of the desired cell.

    sectionController

    The section controller requesting this information.

    Return Value

    The collection reusable view, or nil if not found.

  • Returns the fully visible cells for the given section controller.

    Declaration

    Objective-C

    - (nonnull NSArray<UICollectionViewCell *> *)
        fullyVisibleCellsForSectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    func fullyVisibleCells(for sectionController: IGListSectionController) -> [UICollectionViewCell]

    Parameters

    sectionController

    The section controller requesting this information.

    Return Value

    An array of fully visible cells, or an empty array if none are found.

  • Returns the visible cells for the given section controller.

    Declaration

    Objective-C

    - (nonnull NSArray<UICollectionViewCell *> *)visibleCellsForSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func visibleCells(for sectionController: IGListSectionController) -> [UICollectionViewCell]

    Parameters

    sectionController

    The section controller requesting this information.

    Return Value

    An array of visible cells, or an empty array if none are found.

  • Returns the visible paths for the given section controller.

    Declaration

    Objective-C

    - (nonnull NSArray<NSIndexPath *> *)visibleIndexPathsForSectionController:
        (nonnull IGListSectionController *)sectionController;

    Swift

    func visibleIndexPaths(for sectionController: IGListSectionController) -> [IndexPath]

    Parameters

    sectionController

    The section controller requesting this information.

    Return Value

    An array of visible index paths, or an empty array if none are found.

  • Deselects a cell in the collection.

    Declaration

    Objective-C

    - (void)deselectItemAtIndex:(NSInteger)index
              sectionController:(nonnull IGListSectionController *)sectionController
                       animated:(BOOL)animated;

    Swift

    func deselectItem(at index: Int, sectionController: IGListSectionController, animated: Bool)

    Parameters

    index

    The index of the item to deselect.

    sectionController

    The section controller requesting this information.

    animated

    Pass YES to animate the change, NO otherwise.

  • Selects a cell in the collection.

    Declaration

    Objective-C

    - (void)selectItemAtIndex:(NSInteger)index
            sectionController:(nonnull IGListSectionController *)sectionController
                     animated:(BOOL)animated
               scrollPosition:(UICollectionViewScrollPosition)scrollPosition;

    Swift

    func selectItem(at index: Int, sectionController: IGListSectionController, animated: Bool, scrollPosition: UICollectionView.ScrollPosition)

    Parameters

    index

    The index of the item to select.

    sectionController

    The section controller requesting this information.

    animated

    Pass YES to animate the change, NO otherwise.

    scrollPosition

    An option that specifies where the item should be positioned when scrolling finishes.

  • Dequeues a cell from the collection view reuse pool.

    Note

    This method uses a string representation of the cell class as the identifier.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionViewCell *)
        dequeueReusableCellOfClass:(nonnull Class)cellClass
               withReuseIdentifier:(nullable NSString *)reuseIdentifier
              forSectionController:
                  (nonnull IGListSectionController *)sectionController
                           atIndex:(NSInteger)index;

    Swift

    func dequeueReusableCell(of cellClass: AnyClass, withReuseIdentifier reuseIdentifier: String?, for sectionController: IGListSectionController, at index: Int) -> UICollectionViewCell

    Parameters

    cellClass

    The class of the cell you want to dequeue.

    reuseIdentifier

    A reuse identifier for the specified cell. This parameter may be nil.

    sectionController

    The section controller requesting this information.

    index

    The index of the cell.

    Return Value

    A cell dequeued from the reuse pool or a newly created one.

  • Dequeues a cell from the collection view reuse pool.

    Note

    This method uses a string representation of the cell class as the identifier.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionViewCell *)
        dequeueReusableCellOfClass:(nonnull Class)cellClass
              forSectionController:
                  (nonnull IGListSectionController *)sectionController
                           atIndex:(NSInteger)index;

    Swift

    func dequeueReusableCell(of cellClass: AnyClass, for sectionController: IGListSectionController, at index: Int) -> UICollectionViewCell

    Parameters

    cellClass

    The class of the cell you want to dequeue.

    sectionController

    The section controller requesting this information.

    index

    The index of the cell.

    Return Value

    A cell dequeued from the reuse pool or a newly created one.

  • Dequeues a cell from the collection view reuse pool.

    Note

    This method uses the nib name as the reuse identifier.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionViewCell *)
        dequeueReusableCellWithNibName:(nonnull NSString *)nibName
                                bundle:(nullable NSBundle *)bundle
                  forSectionController:
                      (nonnull IGListSectionController *)sectionController
                               atIndex:(NSInteger)index;

    Swift

    func dequeueReusableCell(withNibName nibName: String, bundle: Bundle?, for sectionController: IGListSectionController, at index: Int) -> UICollectionViewCell

    Parameters

    nibName

    The name of the nib file.

    bundle

    The bundle in which to search for the nib file. If nil, this method searches the main bundle.

    sectionController

    The section controller requesting this information.

    index

    The index of the cell.

    Return Value

    A cell dequeued from the reuse pool or a newly created one.

  • Dequeues a storyboard prototype cell from the collection view reuse pool.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionViewCell *)
        dequeueReusableCellFromStoryboardWithIdentifier:
            (nonnull NSString *)identifier
                                   forSectionController:
                                       (nonnull IGListSectionController *)
                                           sectionController
                                                atIndex:(NSInteger)index;

    Swift

    func dequeueReusableCellFromStoryboard(withIdentifier identifier: String, for sectionController: IGListSectionController, at index: Int) -> UICollectionViewCell

    Parameters

    identifier

    The identifier of the cell prototype in storyboard.

    sectionController

    The section controller requesting this information.

    index

    The index of the cell.

    Return Value

    A cell dequeued from the reuse pool or a newly created one.

  • Dequeues a supplementary view from the collection view reuse pool.

    Note

    This method uses a string representation of the view class and the kind as the identifier.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionReusableView *)
        dequeueReusableSupplementaryViewOfKind:(nonnull NSString *)elementKind
                          forSectionController:
                              (nonnull IGListSectionController *)sectionController
                                         class:(nonnull Class)viewClass
                                       atIndex:(NSInteger)index;

    Swift

    func dequeueReusableSupplementaryView(ofKind elementKind: String, for sectionController: IGListSectionController, class viewClass: AnyClass, at index: Int) -> UICollectionReusableView

    Parameters

    elementKind

    The kind of supplementary view.

    sectionController

    The section controller requesting this information.

    viewClass

    The class of the supplementary view.

    index

    The index of the supplementary view.

    Return Value

    A supplementary view dequeued from the reuse pool or a newly created one.

  • Dequeues a supplementary view from the collection view reuse pool.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionReusableView *)
        dequeueReusableSupplementaryViewFromStoryboardOfKind:
            (nonnull NSString *)elementKind
                                              withIdentifier:
                                                  (nonnull NSString *)identifier
                                        forSectionController:
                                            (nonnull IGListSectionController *)
                                                sectionController
                                                     atIndex:(NSInteger)index;

    Swift

    func dequeueReusableSupplementaryView(fromStoryboardOfKind elementKind: String, withIdentifier identifier: String, for sectionController: IGListSectionController, at index: Int) -> UICollectionReusableView

    Parameters

    elementKind

    The kind of supplementary view.

    identifier

    The identifier of the supplementary view in storyboard.

    sectionController

    The section controller requesting this information.

    index

    The index of the supplementary view.

    Return Value

    A supplementary view dequeued from the reuse pool or a newly created one.

  • Dequeues a supplementary view from the collection view reuse pool.

    Note

    This method uses the nib name as the reuse identifier.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionReusableView *)
        dequeueReusableSupplementaryViewOfKind:(nonnull NSString *)elementKind
                          forSectionController:
                              (nonnull IGListSectionController *)sectionController
                                       nibName:(nonnull NSString *)nibName
                                        bundle:(nullable NSBundle *)bundle
                                       atIndex:(NSInteger)index;

    Swift

    func dequeueReusableSupplementaryView(ofKind elementKind: String, for sectionController: IGListSectionController, nibName: String, bundle: Bundle?, at index: Int) -> UICollectionReusableView

    Parameters

    elementKind

    The kind of supplementary view.

    sectionController

    The section controller requesting this information.

    nibName

    The name of the nib file.

    bundle

    The bundle in which to search for the nib file. If nil, this method searches the main bundle.

    index

    The index of the supplementary view.

    Return Value

    A supplementary view dequeued from the reuse pool or a newly created one.

  • Invalidate the backing UICollectionViewLayout for all items in the section controller.

    Note

    This method can be wrapped in UIView animation APIs to control the duration or perform without animations. This will end up calling -[UICollectionView performBatchUpdates:completion:] internally, so invalidated changes may not be reflected in the cells immediately.

    Declaration

    Objective-C

    - (void)invalidateLayoutForSectionController:
                (nonnull IGListSectionController *)sectionController
                                      completion:
                                          (nullable void (^)(BOOL))completion;

    Swift

    func invalidateLayout(for sectionController: IGListSectionController) async -> Bool

    Parameters

    sectionController

    The section controller that needs invalidating.

    completion

    An optional completion block to execute when the updates are finished.

  • Returns the layout attributes for the item at the specified index in the section controller.

    Declaration

    Objective-C

    - (nullable UICollectionViewLayoutAttributes *)
        layoutAttributesForItemAtIndex:(NSInteger)index
                     sectionController:
                         (nonnull IGListSectionController *)sectionController;

    Swift

    func layoutAttributesForItem(at index: Int, sectionController: IGListSectionController) -> UICollectionViewLayoutAttributes?

    Parameters

    index

    The index of the item whose layout attributes are requested.

    sectionController

    The section controller requesting this information.

    Return Value

    The layout attributes for the item, or nil if the item is not found.

  • Batches and performs many cell-level updates in a single transaction.

    Note

    You should make state changes that impact the number of items in your section controller within the updates block alongside changes on the context object.

    For example, inside your section controllers, you may want to delete and insert into the data source that backs your section controller. For example:

    [self.collectionContext performBatchItemUpdates:^ (id<IGListBatchContext> batchContext>){
      // perform data source changes inside the update block
      [self.items addObject:newItem];
      [self.items removeObjectAtIndex:0];
    
      NSIndexSet *inserts = [NSIndexSet indexSetWithIndex:[self.items count] - 1];
      [batchContext insertInSectionController:self atIndexes:inserts];
    
      NSIndexSet *deletes = [NSIndexSet indexSetWithIndex:0];
      [batchContext deleteInSectionController:self atIndexes:deletes];
    } completion:nil];
    

    Warning

    You must perform data modifications inside the update block. Updates will not be performed synchronously, so you should make sure that your data source changes only when necessary.

    Declaration

    Objective-C

    - (void)performBatchAnimated:(BOOL)animated
                         updates:(nonnull void (^)(id<IGListBatchContext> _Nonnull))
                                     updates
                      completion:(nullable void (^)(BOOL))completion;

    Swift

    func performBatch(animated: Bool, updates: @escaping (any ListBatchContext) -> Void) async -> Bool

    Parameters

    animated

    A flag indicating if the transition should be animated.

    updates

    A block with a context parameter to make mutations.

    completion

    An optional completion block to execute when the updates are finished.

  • Scrolls to the specified section controller in the list.

    Declaration

    Objective-C

    - (void)scrollToSectionController:
                (nonnull IGListSectionController *)sectionController
                              atIndex:(NSInteger)index
                       scrollPosition:(UICollectionViewScrollPosition)scrollPosition
                             animated:(BOOL)animated;

    Swift

    func scroll(to sectionController: IGListSectionController, at index: Int, scrollPosition: UICollectionView.ScrollPosition, animated: Bool)

    Parameters

    sectionController

    The section controller.

    index

    The index of the item in the section controller to which to scroll.

    scrollPosition

    An option that specifies where the item should be positioned when scrolling finishes.

    animated

    A flag indicating if the scrolling should be animated.

  • Returns the index path of the item at the specified point in the collection view.

    Declaration

    Objective-C

    - (nullable NSIndexPath *)indexPathForItemAtPoint:(CGPoint)point;

    Swift

    func indexPathForItem(at point: CGPoint) -> IndexPath?

    Parameters

    point

    The point in the collection view’s coordinate system.

    Return Value

    The index path of the item at the specified point, or nil if no item is found at that location.

  • Converts a point from the coordinate system of a given view to that of the collection view.

    Declaration

    Objective-C

    - (CGPoint)convertPoint:(CGPoint)point fromView:(nullable UIView *)view;

    Swift

    func convert(_ point: CGPoint, from view: UIView?) -> CGPoint

    Parameters

    point

    The point to convert.

    view

    The view from which the point originates. If nil, the point is assumed to be in the window’s coordinate system.

    Return Value

    The converted point in the collection view’s coordinate system.

================================================ FILE: docs/Protocols/IGListCollectionViewDelegateLayout.html ================================================ IGListCollectionViewDelegateLayout Protocol Reference

IGListCollectionViewDelegateLayout

Objective-C

@protocol
    IGListCollectionViewDelegateLayout <UICollectionViewDelegateFlowLayout>

Swift

protocol IGListCollectionViewDelegateLayout : UICollectionViewDelegateFlowLayout

Conform to IGListCollectionViewDelegateLayout to provide customized layout information for a collection view.

  • Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view.

    Declaration

    Objective-C

    - (UICollectionViewLayoutAttributes *)collectionView:
                                              (UICollectionView *)collectionView
                                                  layout:(UICollectionViewLayout *)
                                                             collectionViewLayout
                       customizedInitialLayoutAttributes:
                           (UICollectionViewLayoutAttributes *)attributes
                                             atIndexPath:(NSIndexPath *)indexPath;

    Swift

    func collectionView(_ collectionView: UICollectionView!, layout collectionViewLayout: UICollectionViewLayout!, customizedInitialLayoutAttributes attributes: UICollectionViewLayoutAttributes!, at indexPath: IndexPath!) -> UICollectionViewLayoutAttributes!

    Parameters

    collectionView

    The collection view to perform the transition on.

    collectionViewLayout

    The layout to use with the collection view.

    attributes

    The starting layout information for an item being inserted into the collection view.

    indexPath

    The index path of the item being inserted.

  • Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view.

    Declaration

    Objective-C

    - (UICollectionViewLayoutAttributes *)
                         collectionView:(UICollectionView *)collectionView
                                 layout:
                                     (UICollectionViewLayout *)collectionViewLayout
        customizedFinalLayoutAttributes:
            (UICollectionViewLayoutAttributes *)attributes
                            atIndexPath:(NSIndexPath *)indexPath;

    Swift

    func collectionView(_ collectionView: UICollectionView!, layout collectionViewLayout: UICollectionViewLayout!, customizedFinalLayoutAttributes attributes: UICollectionViewLayoutAttributes!, at indexPath: IndexPath!) -> UICollectionViewLayoutAttributes!

    Parameters

    collectionView

    The collection view to perform the transition on.

    collectionViewLayout

    The layout to use with the collection view.

    attributes

    The final layout information for an item that is about to be removed from the collection view.

    indexPath

    The index path of the item being deleted.

================================================ FILE: docs/Protocols/IGListCollectionViewLayoutCompatible.html ================================================ IGListCollectionViewLayoutCompatible Protocol Reference

IGListCollectionViewLayoutCompatible

Objective-C

@protocol IGListCollectionViewLayoutCompatible <NSObject>

Swift

protocol ListCollectionViewLayoutCompatible : NSObjectProtocol

A protocol for layouts that defines interaction with an IGListCollectionView, for recieving updated section indexes.

  • Called to notify the layout that a specific section was modified before invalidation. This can be used to optimize layout re-calculation.

    Note

    When updating a collection view (ex: calling -insertSections), -invalidateLayoutWithContext gets called on the layout object. However, the invalidation context doesn’t provide details on which index paths are being modified, which typically forces a full layout re-calculation. Layouts can use this method to keep track of which section actually needs to be updated on the following -invalidateLayoutWithContext. See IGListCollectionView.

    Declaration

    Objective-C

    - (void)didModifySection:(NSInteger)modifiedSection;

    Swift

    func didModifySection(_ modifiedSection: Int)

    Parameters

    modifiedSection

    The section that was modified.

================================================ FILE: docs/Protocols/IGListDiffable.html ================================================ IGListDiffable Protocol Reference

IGListDiffable

Objective-C

@protocol IGListDiffable

Swift

protocol ListDiffable

The IGListDiffable protocol provides methods needed to compare the identity and equality of two objects.

  • Returns a key that uniquely identifies the object.

    Note

    Two objects may share the same identifier, but are not equal. A common pattern is to use the NSObject category for automatic conformance. However this means that objects will be identified on their pointer value so finding updates becomes impossible.

    Warning

    This value should never be mutated.

    Declaration

    Objective-C

    - (nonnull id<NSObject>)diffIdentifier;

    Swift

    func diffIdentifier() -> any NSObjectProtocol

    Return Value

    A key that can be used to uniquely identify the object.

  • Returns whether the receiver and a given object are equal.

    Declaration

    Objective-C

    - (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object;

    Swift

    func isEqual(toDiffableObject object: (any ListDiffable)?) -> Bool

    Parameters

    object

    The object to be compared to the receiver.

    Return Value

    YES if the receiver and object are equal, otherwise NO.

================================================ FILE: docs/Protocols/IGListDisplayDelegate.html ================================================ IGListDisplayDelegate Protocol Reference

IGListDisplayDelegate

Objective-C

@protocol IGListDisplayDelegate <NSObject>

Swift

@MainActor protocol ListDisplayDelegate : NSObjectProtocol

Implement this protocol to receive display events for a section controller when it is on screen.

  • Tells the delegate that the specified section controller is about to be displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        willDisplaySectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, willDisplay sectionController: IGListSectionController)

    Parameters

    listAdapter

    The list adapter for the section controller.

    sectionController

    The section controller about to be displayed.

  • Tells the delegate that the specified section controller is no longer being displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDisplayingSectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didEndDisplaying sectionController: IGListSectionController)

    Parameters

    listAdapter

    The list adapter for the section controller.

    sectionController

    The section controller that is no longer displayed.

  • Tells the delegate that a cell in the specified list is about to be displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        willDisplaySectionController:
            (nonnull IGListSectionController *)sectionController
                                cell:(nonnull UICollectionViewCell *)cell
                             atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, willDisplay sectionController: IGListSectionController, cell: UICollectionViewCell, at index: Int)

    Parameters

    listAdapter

    The list adapter in which the cell will display.

    sectionController

    The section controller that is displaying the cell.

    cell

    The cell about to be displayed.

    index

    The index of the cell in the section.

  • Tells the delegate that a cell in the specified list is no longer being displayed.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDisplayingSectionController:
            (nonnull IGListSectionController *)sectionController
                                     cell:(nonnull UICollectionViewCell *)cell
                                  atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didEndDisplaying sectionController: IGListSectionController, cell: UICollectionViewCell, at index: Int)

    Parameters

    listAdapter

    The list adapter in which the cell was displayed.

    sectionController

    The section controller that is no longer displaying the cell.

    cell

    The cell that is no longer displayed.

    index

    The index of the cell in the section.

================================================ FILE: docs/Protocols/IGListScrollDelegate.html ================================================ IGListScrollDelegate Protocol Reference

IGListScrollDelegate

Objective-C

@protocol IGListScrollDelegate <NSObject>

Swift

protocol ListScrollDelegate : NSObjectProtocol

Implement this protocol to receive display events for a section controller when it is on screen.

  • Tells the delegate that the section controller was scrolled on screen.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didScrollSectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didScroll sectionController: IGListSectionController)

    Parameters

    listAdapter

    The list adapter whose collection view was scrolled.

    sectionController

    The visible section controller that was scrolled.

  • Tells the delegate that the section controller will be dragged on screen.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        willBeginDraggingSectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, willBeginDragging sectionController: IGListSectionController)

    Parameters

    listAdapter

    The list adapter whose collection view will drag.

    sectionController

    The visible section controller that will drag.

  • Tells the delegate that the section controller did end dragging on screen.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDraggingSectionController:
            (nonnull IGListSectionController *)sectionController
                         willDecelerate:(BOOL)decelerate;

    Swift

    func listAdapter(_ listAdapter: IGListAdapter, didEndDragging sectionController: IGListSectionController, willDecelerate decelerate: Bool)

    Parameters

    listAdapter

    The list adapter whose collection view ended dragging.

    sectionController

    The visible section controller that ended dragging.

    decelerate

    ‘Yes’ if the scrolling movement will continue, but decelerate, after a touch-up gesture during a dragging operation. If the value is ‘No’, scrolling stops immediately upon touch-up.

  • Tells the delegate that the section controller did end decelerating on screen.

    Note

    This method is @optional until the next breaking-change release.

    Declaration

    Objective-C

    - (void)listAdapter:(nonnull IGListAdapter *)listAdapter
        didEndDeceleratingSectionController:
            (nonnull IGListSectionController *)sectionController;

    Swift

    optional func listAdapter(_ listAdapter: IGListAdapter, didEndDeceleratingSectionController sectionController: IGListSectionController)

    Parameters

    listAdapter

    The list adapter whose collection view ended decelerating.

    sectionController

    The visible section controller that ended decelerating.

================================================ FILE: docs/Protocols/IGListSingleSectionControllerDelegate.html ================================================ IGListSingleSectionControllerDelegate Protocol Reference

IGListSingleSectionControllerDelegate

Objective-C

@protocol IGListSingleSectionControllerDelegate <NSObject>

Swift

@MainActor protocol ListSingleSectionControllerDelegate : NSObjectProtocol

A delegate that can receive selection events on an IGListSingleSectionController.

  • Tells the delegate that the section controller was selected.

    Declaration

    Objective-C

    - (void)didSelectSectionController:
                (nonnull IGListSingleSectionController *)sectionController
                            withObject:(nonnull id)object;

    Swift

    func didSelect(_ sectionController: ListSingleSectionController, with object: Any)

    Parameters

    sectionController

    The section controller that was selected.

    object

    The model for the given section.

  • Tells the delegate that the section controller was deselected.

    Note

    Method is @optional until the 4.0.0 release where it will become required.

    Declaration

    Objective-C

    - (void)didDeselectSectionController:
                (nonnull IGListSingleSectionController *)sectionController
                              withObject:(nonnull id)object;

    Swift

    optional func didDeselect(_ sectionController: ListSingleSectionController, with object: Any)

    Parameters

    sectionController

    The section controller that was deselected.

    object

    The model for the given section.

================================================ FILE: docs/Protocols/IGListSupplementaryViewSource.html ================================================ IGListSupplementaryViewSource Protocol Reference

IGListSupplementaryViewSource

Objective-C

@protocol IGListSupplementaryViewSource <NSObject>

Swift

@MainActor protocol ListSupplementaryViewSource : NSObjectProtocol

Conform to this protocol to provide information about a list’s supplementary views. This data is used in IGListAdapter which then configures and maintains a UICollectionView. The supplementary API reflects that in UICollectionView, UICollectionViewLayout, and UICollectionViewDataSource.

  • Asks the SupplementaryViewSource for an array of supported element kinds.

    Declaration

    Objective-C

    - (nonnull NSArray<NSString *> *)supportedElementKinds;

    Swift

    func supportedElementKinds() -> [String]

    Return Value

    An array of element kind strings that the supplementary source handles.

  • Asks the SupplementaryViewSource for a configured supplementary view for the specified kind and index.

    Note

    This is your opportunity to do any supplementary view setup and configuration.

    Warning

    You should never allocate new views in this method. Instead deque a view from the IGListCollectionContext.

    Declaration

    Objective-C

    - (nonnull __kindof UICollectionReusableView *)
        viewForSupplementaryElementOfKind:(nonnull NSString *)elementKind
                                  atIndex:(NSInteger)index;

    Swift

    func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView

    Parameters

    elementKind

    The kind of supplementary view being requested

    index

    The index for the supplementary view being requested.

  • Asks the SupplementaryViewSource for the size of a supplementary view for the given kind and index path.

    Declaration

    Objective-C

    - (CGSize)sizeForSupplementaryViewOfKind:(nonnull NSString *)elementKind
                                     atIndex:(NSInteger)index;

    Swift

    func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize

    Parameters

    elementKind

    The kind of supplementary view.

    index

    The index of the requested view.

    Return Value

    The size for the supplementary view.

================================================ FILE: docs/Protocols/IGListTransitionDelegate.html ================================================ IGListTransitionDelegate Protocol Reference

IGListTransitionDelegate

Objective-C

@protocol IGListTransitionDelegate <NSObject>

Swift

protocol IGListTransitionDelegate

Conform to IGListTransitionDelegate to provide customized layout information for a collection view.

  • Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view.

    Declaration

    Objective-C

    - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter
                    customizedInitialLayoutAttributes:
                        (UICollectionViewLayoutAttributes *)attributes
                                    sectionController:
                                        (IGListSectionController *)sectionController
                                              atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: Any!, customizedInitialLayoutAttributes attributes: Any!, sectionController: Any!, atIndex index: Any!) -> Any!

    Parameters

    listAdapter

    The adapter controlling the list.

    attributes

    The starting layout information for an item being inserted into the collection view.

    sectionController

    The section controller to perform the transition on.

    index

    The index of the item being inserted.

  • Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view.

    Declaration

    Objective-C

    - (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter
                      customizedFinalLayoutAttributes:
                          (UICollectionViewLayoutAttributes *)attributes
                                    sectionController:
                                        (IGListSectionController *)sectionController
                                              atIndex:(NSInteger)index;

    Swift

    func listAdapter(_ listAdapter: Any!, customizedFinalLayoutAttributes attributes: Any!, sectionController: Any!, atIndex index: Any!) -> Any!

    Parameters

    listAdapter

    The adapter controlling the list.

    attributes

    The final layout information for an item that is about to be removed from the collection view.

    sectionController

    The section controller to perform the transition on.

    index

    The index of the item being deleted.

================================================ FILE: docs/Protocols/IGListUpdatingDelegate.html ================================================ IGListUpdatingDelegate Protocol Reference

IGListUpdatingDelegate

Objective-C

@protocol IGListUpdatingDelegate <NSObject>

Swift

protocol ListUpdatingDelegate : NSObjectProtocol

Implement this protocol in order to handle both section and row based update events. Implementation should forward or coalesce these events to a backing store or collection.

  • Asks the delegate for the pointer functions for looking up an object in a collection.

    Note

    Since the updating delegate is responsible for transitioning between object sets, it becomes the “source of truth” for how objects and their corresponding section controllers are mapped. This allows the updater to control if objects are looked up by pointer, or more traditionally, with -hash/-isEqual.

    For behavior similar to NSDictionary, simply return +[NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality].

    Declaration

    Objective-C

    - (nonnull NSPointerFunctions *)objectLookupPointerFunctions;

    Swift

    func objectLookupPointerFunctions() -> NSPointerFunctions

    Return Value

    Pointer functions for looking up an object in a collection.

  • Perform a section update from an old array of objects to a new one.

    Note

    Implementations determine how to transition between objects. You can perform a diff on the objects, reload each section, or simply call -reloadData on the collection view. In the end, the collection view must be setup with a section for each object in the toObjects array.

    The applySectionDataBlock block should be called prior to making any UICollectionView updates, passing in the toObjects that the updater is applying.

    Declaration

    Objective-C

    - (void)performUpdateWithCollectionViewBlock:
                (nonnull IGListCollectionViewBlock)collectionViewBlock
                                        animated:(BOOL)animated
                                sectionDataBlock:(nonnull IGListTransitionDataBlock)
                                                     sectionDataBlock
                           applySectionDataBlock:
                               (nonnull IGListTransitionDataApplyBlock)
                                   applySectionDataBlock
                                      completion:(nullable IGListUpdatingCompletion)
                                                     completion;

    Swift

    func performUpdate(collectionViewBlock: @escaping ListCollectionViewBlock, animated: Bool, sectionDataBlock: @escaping ListTransitionDataBlock, applySectionDataBlock: @escaping ListTransitionDataApplyBlock) async -> Bool

    Parameters

    collectionViewBlock

    A block returning the collecion view to perform updates on.

    animated

    A flag indicating if the transition should be animated.

    sectionDataBlock

    A block that returns the section information (ex: from and to objects)

    applySectionDataBlock

    A block that must be called when the adapter applies changes to the collection view.

    completion

    A completion block to execute when the update is finished.

  • Perform an item update block in the collection view.

    Declaration

    Objective-C

    - (void)performUpdateWithCollectionViewBlock:
                (nonnull IGListCollectionViewBlock)collectionViewBlock
                                        animated:(BOOL)animated
                                     itemUpdates:
                                         (nonnull IGListItemUpdateBlock)itemUpdates
                                      completion:(nullable IGListUpdatingCompletion)
                                                     completion;

    Swift

    func performUpdate(collectionViewBlock: @escaping ListCollectionViewBlock, animated: Bool, itemUpdates: @escaping ListItemUpdateBlock) async -> Bool

    Parameters

    collectionViewBlock

    A block returning the collecion view to perform updates on.

    animated

    A flag indicating if the transition should be animated.

    itemUpdates

    A block containing all of the updates.

    completion

    A completion block to execute when the update is finished.

  • Perform a [UICollectionView setDataSource:...] swap within this block. It gives the updater the chance to cancel or execute any on-going updates. The block should be executed synchronously.

    Declaration

    Objective-C

    - (void)performDataSourceChange:(nonnull IGListDataSourceChangeBlock)block;

    Swift

    func performDataSourceChange(_ block: @escaping ListDataSourceChangeBlock)

    Parameters

    block

    The block that will actuallty change the dataSource

  • Completely reload data in the collection.

    Declaration

    Objective-C

    - (void)reloadDataWithCollectionViewBlock:
                (nonnull IGListCollectionViewBlock)collectionViewBlock
                            reloadUpdateBlock:
                                (nonnull IGListReloadUpdateBlock)reloadUpdateBlock
                                   completion:(nullable IGListUpdatingCompletion)
                                                  completion;

    Swift

    func reloadData(collectionViewBlock: @escaping ListCollectionViewBlock, reloadUpdate reloadUpdateBlock: @escaping ListReloadUpdateBlock) async -> Bool

    Parameters

    collectionViewBlock

    A block returning the collecion view to reload.

    reloadUpdateBlock

    A block that must be called when the adapter reloads the collection view.

    completion

    A completion block to execute when the reload is finished.

  • Tells the delegate to perform item inserts at the given index paths.

    Declaration

    Objective-C

    - (void)insertItemsIntoCollectionView:(nonnull UICollectionView *)collectionView
                               indexPaths:
                                   (nonnull NSArray<NSIndexPath *> *)indexPaths;

    Swift

    func insertItems(into collectionView: UICollectionView, indexPaths: [IndexPath])

    Parameters

    collectionView

    The collection view on which to perform the transition.

    indexPaths

    The index paths to insert items into.

  • Tells the delegate to perform item deletes at the given index paths.

    Declaration

    Objective-C

    - (void)deleteItemsFromCollectionView:(nonnull UICollectionView *)collectionView
                               indexPaths:
                                   (nonnull NSArray<NSIndexPath *> *)indexPaths;

    Swift

    func deleteItems(from collectionView: UICollectionView, indexPaths: [IndexPath])

    Parameters

    collectionView

    The collection view on which to perform the transition.

    indexPaths

    The index paths to delete items from.

  • Tells the delegate to move an item from and to given index paths.

    Declaration

    Objective-C

    - (void)moveItemInCollectionView:(nonnull UICollectionView *)collectionView
                       fromIndexPath:(nonnull NSIndexPath *)fromIndexPath
                         toIndexPath:(nonnull NSIndexPath *)toIndexPath;

    Swift

    func moveItem(in collectionView: UICollectionView, from fromIndexPath: IndexPath, to toIndexPath: IndexPath)

    Parameters

    collectionView

    The collection view on which to perform the transition.

    fromIndexPath

    The source index path of the item to move.

    toIndexPath

    The destination index path of the item to move.

  • Tells the delegate to reload an item from and to given index paths.

    Note

    Since UICollectionView is unable to handle calling -[UICollectionView reloadItemsAtIndexPaths:] safely while also executing insert and delete operations in the same batch updates, the updater must know about the origin and destination of the reload to perform a safe transition.

    Declaration

    Objective-C

    - (void)reloadItemInCollectionView:(nonnull UICollectionView *)collectionView
                         fromIndexPath:(nonnull NSIndexPath *)fromIndexPath
                           toIndexPath:(nonnull NSIndexPath *)toIndexPath;

    Swift

    func reloadItem(in collectionView: UICollectionView, from fromIndexPath: IndexPath, to toIndexPath: IndexPath)

    Parameters

    collectionView

    The collection view on which to perform the transition.

    fromIndexPath

    The source index path of the item to reload.

    toIndexPath

    The destination index path of the item to reload.

  • Tells the delegate to move a section from and to given indexes.

    Declaration

    Objective-C

    - (void)moveSectionInCollectionView:(nonnull UICollectionView *)collectionView
                              fromIndex:(NSInteger)fromIndex
                                toIndex:(NSInteger)toIndex;

    Swift

    func moveSection(in collectionView: UICollectionView, from fromIndex: Int, to toIndex: Int)

    Parameters

    collectionView

    The collection view on which to perform the transition.

    fromIndex

    The source index of the section to move.

    toIndex

    The destination index of the section to move.

  • Completely reload each section in the collection view.

    Declaration

    Objective-C

    - (void)reloadCollectionView:(nonnull UICollectionView *)collectionView
                        sections:(nonnull NSIndexSet *)sections;

    Swift

    func reload(_ collectionView: UICollectionView, sections: IndexSet)

    Parameters

    collectionView

    The collection view to reload.

    sections

    The sections to reload.

  • True if the updater is currently updating the source of truth, like executing applySectionDataBlock and itemUpdates just before updating the UICollectionView.

    Declaration

    Objective-C

    - (BOOL)isInDataUpdateBlock;

    Swift

    func isInDataUpdateBlock() -> Bool
  • Called when the updater detects an imminent crash, such as when a section controller returns a nil cell. This provides an opportunity to log diagnostic information before the crash occurs.

    Declaration

    Objective-C

    - (void)willCrashWithCollectionView:(nonnull UICollectionView *)collectionView
                 sectionControllerClass:(nullable Class)sectionControllerClass;

    Swift

    func willCrash(with collectionView: UICollectionView, sectionControllerClass: AnyClass?)

    Parameters

    collectionView

    The collection view involved in the crash.

    sectionControllerClass

    The class of the section controller that caused the issue, if available.

================================================ FILE: docs/Protocols/IGListWorkingRangeDelegate.html ================================================ IGListWorkingRangeDelegate Protocol Reference

IGListWorkingRangeDelegate

Objective-C

@protocol IGListWorkingRangeDelegate <NSObject>

Swift

@MainActor protocol ListWorkingRangeDelegate : NSObjectProtocol

Implement this protocol to receive working range events for a list.

The working range is a range near the viewport in which you can begin preparing content for display. For example, you could begin decoding images, or warming text caches.

================================================ FILE: docs/Protocols.html ================================================ Protocols Reference

Protocols

The following protocols are available globally.

  • The IGListDiffable protocol provides methods needed to compare the identity and equality of two objects.

    See more

    Declaration

    Objective-C

    @protocol IGListDiffable

    Swift

    protocol ListDiffable
  • Implement this protocol to provide data to an IGListAdapter.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterDataSource <NSObject>

    Swift

    @MainActor protocol ListAdapterDataSource : NSObjectProtocol
  • Conform to IGListAdapterDelegate to receive display events for objects in a list.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterDelegate <NSObject>

    Swift

    @MainActor protocol ListAdapterDelegate : NSObjectProtocol
  • Conform to IGListAdapterMoveDelegate to receive interactive reordering requests.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterMoveDelegate <NSObject>

    Swift

    @MainActor protocol ListAdapterMoveDelegate : NSObjectProtocol
  • IGListAdapterPerformanceDelegate can be used to measure cell dequeue, display, size, and scroll callbacks.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterPerformanceDelegate <NSObject>

    Swift

    protocol ListAdapterPerformanceDelegate : NSObjectProtocol
  • Conform to this protocol to receive events about IGListAdapter updates.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterUpdateListener <NSObject>

    Swift

    @MainActor protocol ListAdapterUpdateListener : NSObjectProtocol
  • A protocol that receives events about IGListAdapterUpdater operations.

    See more

    Declaration

    Objective-C

    @protocol IGListAdapterUpdaterDelegate <NSObject>

    Swift

    protocol ListAdapterUpdaterDelegate : NSObjectProtocol
  • Objects conforming to the IGListBatchContext protocol provide a way for section controllers to mutate their cells or reload everything within the section.

    See more

    Declaration

    Objective-C

    @protocol IGListBatchContext <NSObject>

    Swift

    protocol ListBatchContext : NSObjectProtocol
  • A protocol for cells that configure themselves given a view model.

    See more

    Declaration

    Objective-C

    @protocol IGListBindable <NSObject>

    Swift

    @MainActor protocol ListBindable : NSObjectProtocol
  • A protocol that returns data to power cells in an IGListBindingSectionController.

    See more

    Declaration

    Objective-C

    @protocol IGListBindingSectionControllerDataSource <NSObject>

    Swift

    @MainActor protocol ListBindingSectionControllerDataSource : NSObjectProtocol
  • A protocol that handles cell selection events in an IGListBindingSectionController.

    See more

    Declaration

    Objective-C

    @protocol IGListBindingSectionControllerSelectionDelegate <NSObject>

    Swift

    @MainActor protocol ListBindingSectionControllerSelectionDelegate : NSObjectProtocol
  • The collection context provides limited access to the collection-related information that section controllers need for operations like sizing, dequeuing cells, inserting, deleting, reloading, etc.

    See more

    Declaration

    Objective-C

    @protocol IGListCollectionContext <NSObject>

    Swift

    protocol ListCollectionContext : NSObjectProtocol
  • Conform to IGListCollectionViewDelegateLayout to provide customized layout information for a collection view.

    See more

    Declaration

    Objective-C

    @protocol
        IGListCollectionViewDelegateLayout <UICollectionViewDelegateFlowLayout>

    Swift

    protocol IGListCollectionViewDelegateLayout : UICollectionViewDelegateFlowLayout
  • A protocol for layouts that defines interaction with an IGListCollectionView, for recieving updated section indexes.

    See more

    Declaration

    Objective-C

    @protocol IGListCollectionViewLayoutCompatible <NSObject>

    Swift

    protocol ListCollectionViewLayoutCompatible : NSObjectProtocol
  • Implement this protocol to receive display events for a section controller when it is on screen.

    See more

    Declaration

    Objective-C

    @protocol IGListDisplayDelegate <NSObject>

    Swift

    @MainActor protocol ListDisplayDelegate : NSObjectProtocol
  • Implement this protocol to receive display events for a section controller when it is on screen.

    See more

    Declaration

    Objective-C

    @protocol IGListScrollDelegate <NSObject>

    Swift

    protocol ListScrollDelegate : NSObjectProtocol
  • A delegate that can receive selection events on an IGListSingleSectionController.

    See more

    Declaration

    Objective-C

    @protocol IGListSingleSectionControllerDelegate <NSObject>

    Swift

    @MainActor protocol ListSingleSectionControllerDelegate : NSObjectProtocol
  • Conform to this protocol to provide information about a list’s supplementary views. This data is used in IGListAdapter which then configures and maintains a UICollectionView. The supplementary API reflects that in UICollectionView, UICollectionViewLayout, and UICollectionViewDataSource.

    See more

    Declaration

    Objective-C

    @protocol IGListSupplementaryViewSource <NSObject>

    Swift

    @MainActor protocol ListSupplementaryViewSource : NSObjectProtocol
  • Conform to IGListTransitionDelegate to provide customized layout information for a collection view.

    See more

    Declaration

    Objective-C

    @protocol IGListTransitionDelegate <NSObject>

    Swift

    protocol IGListTransitionDelegate
  • Implement this protocol in order to handle both section and row based update events. Implementation should forward or coalesce these events to a backing store or collection.

    See more

    Declaration

    Objective-C

    @protocol IGListUpdatingDelegate <NSObject>

    Swift

    protocol ListUpdatingDelegate : NSObjectProtocol
  • Implement this protocol to receive working range events for a list.

    The working range is a range near the viewport in which you can begin preparing content for display. For example, you could begin decoding images, or warming text caches.

    See more

    Declaration

    Objective-C

    @protocol IGListWorkingRangeDelegate <NSObject>

    Swift

    @MainActor protocol ListWorkingRangeDelegate : NSObjectProtocol
================================================ FILE: docs/Structs/IGListAdaptiveCoalescingExperimentConfig.html ================================================ IGListAdaptiveCoalescingExperimentConfig Structure Reference

IGListAdaptiveCoalescingExperimentConfig

Objective-C

struct IGListAdaptiveCoalescingExperimentConfig {}

Swift

struct IGListAdaptiveCoalescingExperimentConfig

Customize how coalescing works to speed up some updates

  • Enable adaptive coalescing, where we try to mininimize the update delay

    Declaration

    Objective-C

    BOOL enabled

    Swift

    var enabled: ObjCBool
  • Start coalescing if the last update was within this interval

    Declaration

    Objective-C

    NSTimeInterval minInterval

    Swift

    var minInterval: TimeInterval
  • If we need to coalesce, increase the interval by this much for next time.

    Declaration

    Objective-C

    NSTimeInterval intervalIncrement

    Swift

    var intervalIncrement: TimeInterval
  • This is the maximum coalesce interval, so the slowest and update can wait.

    Declaration

    Objective-C

    NSTimeInterval maxInterval

    Swift

    var maxInterval: TimeInterval
  • Coalece using maxInterval if view is not visible according to IGListViewVisibilityTracker

    Declaration

    Objective-C

    BOOL useMaxIntervalWhenViewNotVisible

    Swift

    var useMaxIntervalWhenViewNotVisible: ObjCBool
================================================ FILE: docs/Structs/IGListAdaptiveDiffingExperimentConfig.html ================================================ IGListAdaptiveDiffingExperimentConfig Structure Reference

IGListAdaptiveDiffingExperimentConfig

Objective-C

struct IGListAdaptiveDiffingExperimentConfig {}

Swift

struct IGListAdaptiveDiffingExperimentConfig

Customize how diffing is performed

  • Enabled experimental code path. This needs to be enabled for the other properties to take effect.

    Declaration

    Objective-C

    BOOL enabled

    Swift

    var enabled: ObjCBool
  • Enable higher background thread priority

    Declaration

    Objective-C

    BOOL higherQOSEnabled

    Swift

    var higherQOSEnabled: ObjCBool
  • If both item counts are under this number, we’ll run the diffing on the main thread.

    Declaration

    Objective-C

    NSInteger maxItemCountToRunOnMain

    Swift

    var maxItemCountToRunOnMain: Int
  • Lower QOS if view is not visible according to IGListViewVisibilityTracker

    Declaration

    Objective-C

    BOOL lowerPriorityWhenViewNotVisible

    Swift

    var lowerPriorityWhenViewNotVisible: ObjCBool
================================================ FILE: docs/Structs/IGListCollectionScrollingTraits.html ================================================ IGListCollectionScrollingTraits Structure Reference

IGListCollectionScrollingTraits

Objective-C

struct IGListCollectionScrollingTraits {}

Swift

struct IGListCollectionScrollingTraits

The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view.

  • returns YES if user has touched. may not yet have started dragging.

    Declaration

    Objective-C

    _Bool isTracking

    Swift

    var isTracking: Bool
  • returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging

    Declaration

    Objective-C

    _Bool isDragging

    Swift

    var isDragging: Bool
  • returns YES if user isn’t dragging (touch up) but scroll view is still moving.

    Declaration

    Objective-C

    _Bool isDecelerating

    Swift

    var isDecelerating: Bool
================================================ FILE: docs/Structs.html ================================================ Structures Reference

Structures

The following structures are available globally.

  • Customize how diffing is performed

    See more

    Declaration

    Objective-C

    struct IGListAdaptiveDiffingExperimentConfig {}

    Swift

    struct IGListAdaptiveDiffingExperimentConfig
  • Customize how coalescing works to speed up some updates

    See more

    Declaration

    Objective-C

    struct IGListAdaptiveCoalescingExperimentConfig {}

    Swift

    struct IGListAdaptiveCoalescingExperimentConfig
  • The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view.

    See more

    Declaration

    Objective-C

    struct IGListCollectionScrollingTraits {}

    Swift

    struct IGListCollectionScrollingTraits
================================================ FILE: docs/Type Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html ================================================ IGListAdaptiveCoalescingExperimentConfig Structure Reference

IGListAdaptiveCoalescingExperimentConfig

Customize how coalescing works to speed up some updates

  • Enable adaptive coalescing, where we try to mininimize the update delay

    Declaration

    Objective-C

    BOOL enabled

    Swift

    var enabled: ObjCBool
  • Start coalescing if the last update was within this interval

    Declaration

    Objective-C

    NSTimeInterval minInterval

    Swift

    var minInterval: TimeInterval
  • If we need to coalesce, increase the interval by this much for next time.

    Declaration

    Objective-C

    NSTimeInterval intervalIncrement

    Swift

    var intervalIncrement: TimeInterval
  • This is the maximum coalesce interval, so the slowest and update can wait.

    Declaration

    Objective-C

    NSTimeInterval maxInterval

    Swift

    var maxInterval: TimeInterval
  • Coalece using maxInterval if view is not visible according to IGListViewVisibilityTracker

    Declaration

    Objective-C

    BOOL useMaxIntervalWhenViewNotVisible

    Swift

    var useMaxIntervalWhenViewNotVisible: ObjCBool
================================================ FILE: docs/Type Definitions/IGListAdaptiveCoalescingExperimentConfig.html ================================================ IGListAdaptiveCoalescingExperimentConfig Type Definition Reference

IGListAdaptiveCoalescingExperimentConfig

Customize how coalescing works to speed up some updates

================================================ FILE: docs/Type Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html ================================================ IGListAdaptiveDiffingExperimentConfig Structure Reference

IGListAdaptiveDiffingExperimentConfig

Customize how diffing is performed

  • Enabled experimental code path. This needs to be enabled for the other properties to take effect.

    Declaration

    Objective-C

    BOOL enabled

    Swift

    var enabled: ObjCBool
  • Enable higher background thread priority

    Declaration

    Objective-C

    BOOL higherQOSEnabled

    Swift

    var higherQOSEnabled: ObjCBool
  • If both item counts are under this number, we’ll run the diffing on the main thread.

    Declaration

    Objective-C

    NSInteger maxItemCountToRunOnMain

    Swift

    var maxItemCountToRunOnMain: Int
  • Lower QOS if view is not visible according to IGListViewVisibilityTracker

    Declaration

    Objective-C

    BOOL lowerPriorityWhenViewNotVisible

    Swift

    var lowerPriorityWhenViewNotVisible: ObjCBool
================================================ FILE: docs/Type Definitions/IGListAdaptiveDiffingExperimentConfig.html ================================================ IGListAdaptiveDiffingExperimentConfig Type Definition Reference
================================================ FILE: docs/Type Definitions/IGListCollectionScrollingTraits/IGListCollectionScrollingTraits.html ================================================ IGListCollectionScrollingTraits Structure Reference

IGListCollectionScrollingTraits

The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view.

  • returns YES if user has touched. may not yet have started dragging.

    Declaration

    Objective-C

    _Bool isTracking

    Swift

    var isTracking: Bool
  • returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging

    Declaration

    Objective-C

    _Bool isDragging

    Swift

    var isDragging: Bool
  • returns YES if user isn’t dragging (touch up) but scroll view is still moving.

    Declaration

    Objective-C

    _Bool isDecelerating

    Swift

    var isDecelerating: Bool
================================================ FILE: docs/Type Definitions/IGListCollectionScrollingTraits.html ================================================ IGListCollectionScrollingTraits Type Definition Reference

IGListCollectionScrollingTraits

The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view.

================================================ FILE: docs/Type Definitions.html ================================================ Type Definitions Reference

Type Definitions

The following type definitions are available globally.

  • Customize how diffing is performed

    See more
  • Customize how coalescing works to speed up some updates

    See more
  • A block to execute when the list updates are completed.

    Declaration

    Objective-C

    typedef void (^IGListUpdaterCompletion)(BOOL)

    Swift

    typealias ListUpdaterCompletion = (Bool) -> Void

    Parameters

    finished

    Specifies whether or not the update animations completed successfully.

  • The current scrolling traits of the underlying collection view. The attributes are always equal to their corresponding properties on the underlying collection view.

    See more

    Declaration

  • A block used to configure cells.

    Declaration

    Objective-C

    typedef void (^IGListSingleSectionCellConfigureBlock)(
        id _Nonnull, __kindof UICollectionViewCell *_Nonnull)

    Swift

    typealias ListSingleSectionCellConfigureBlock = (Any, UICollectionViewCell) -> Void

    Parameters

    item

    The model with which to configure the cell.

    cell

    The cell to configure.

  • A block that returns the size for the cell given the collection context.

    Declaration

    Objective-C

    typedef CGSize (^IGListSingleSectionCellSizeBlock)(
        id _Nonnull, id<IGListCollectionContext> _Nullable)

    Swift

    typealias ListSingleSectionCellSizeBlock = (Any, (any ListCollectionContext)?) -> CGSize

    Parameters

    item

    The model for the section.

    collectionContext

    The collection context for the section.

    Return Value

    The size for the cell.

  • A completion block to execute when updates are finished.

    Declaration

    Objective-C

    typedef void (^IGListUpdatingCompletion)(BOOL)

    Swift

    typealias ListUpdatingCompletion = (Bool) -> Void

    Parameters

    finished

    Specifies whether or not the update finished.

  • A block to be called when the adapter applies changes to the collection view.

    Declaration

    Objective-C

    typedef void (^IGListObjectTransitionBlock)(NSArray *_Nonnull)

    Swift

    typealias ListObjectTransitionBlock = ([Any]) -> Void

    Parameters

    toObjects

    The new objects in the collection.

  • A block that contains all of the updates.

    Declaration

    Objective-C

    typedef void (^IGListItemUpdateBlock)(void)

    Swift

    typealias ListItemUpdateBlock = () -> Void
  • A block to be called when an adapter reloads the collection view.

    Declaration

    Objective-C

    typedef void (^IGListReloadUpdateBlock)(void)

    Swift

    typealias ListReloadUpdateBlock = () -> Void
  • A block that returns an array of objects to transition to.

    Declaration

    Objective-C

    typedef NSArray *_Nullable (^IGListToObjectBlock)(void)

    Swift

    typealias ListToObjectBlock = () -> [Any]?
  • A block that returns a collection view to perform updates on.

    Declaration

    Objective-C

    typedef UICollectionView *_Nullable (^IGListCollectionViewBlock)(void)

    Swift

    typealias ListCollectionViewBlock = () -> UICollectionView?
  • A block that applies a UICollectionView dataSource change

    Declaration

    Objective-C

    typedef void (^IGListDataSourceChangeBlock)(void)

    Swift

    typealias ListDataSourceChangeBlock = () -> Void
  • A block that returns the IGListTransitionData needed before an update.

    Declaration

    Objective-C

    typedef IGListTransitionData *_Nullable (^IGListTransitionDataBlock)(void)

    Swift

    typealias ListTransitionDataBlock = () -> IGListTransitionData?
  • A block to be called when the adapter applies changes to the collection view.

    Declaration

    Objective-C

    typedef void (^IGListTransitionDataApplyBlock)(IGListTransitionData *_Nonnull)

    Swift

    typealias ListTransitionDataApplyBlock = (IGListTransitionData) -> Void

    Parameters

    data

    The new data that contains the from/to objects.

================================================ FILE: docs/best-practices-and-faq.html ================================================ Best Practices and FAQ Reference

Best Practices and FAQs

This guide provides notes and details on best practices in using IGListKit, general tips, and answers to FAQs.

Best Practices

  • We recommend adding an assert to check -isKindOfClass: on the object you receive in -didUpdateToObject: in your section controllers. This makes it easy to track down easily-overlooked mistakes in your IGListAdapaterDataSource implementation. If this assert is ever hit, that means IGListKit has sent your section controller the incorrect type of object. This would only happen if your objects provide non-unique diff identifiers.
// Objective-C
- (void)didUpdateToObject:(id)object {
    NSParameterAssert([object isKindOfClass:[MyModelClass class]]);
    _myModel = object;
}
// Swift
func didUpdate(to object: Any) {
    precondition(object is MyModelClass)
    myModel = object as! MyModelClass
}
  • Make sure your -diffIdentifier implementation returns a unique identifier for each object.

  • We highly recommend using single-item sections when possible. That is, each section controller manages a single model (which may have one or multiple cells). This gives you the greatest amount of flexibility, modularity, and re-use for your components.

Frequently asked questions

I upgraded IGListKit and now everything is broken!

Check out our migration guide to make upgrading easier.

How do you implement separators between cells?

See discussion in #329

How do I fix the error Could not build Objective-C module 'IGListKit'?

See discussion in #316

The documentation and examples have <X> feature or changes, but I don’t have it in my version. Why?

This feature is on the main branch only and hasn’t been officially tagged and released. If you need to, you can install from the main branch.

Does IGListKit work with…?

Yes.

Does IGListKit work with UITableView?

No, but you can install the diffing subspec via CocoaPods.

What’s the purpose of IGListCollectionView?

Historically, we used this subclass to gain compile-time safety to prevent disallowed methods from being called on UICollectionView, because IGListKit handles model and view updates. However, it has since been removed. See discussion at #409.

How can I manage cell selection and deselection?

See discussion at #184.

I have a huge data set and -performUpdatesAnimated: completion: is super slow. What do I do?

If you have multiple thousands of items and you cannot batch them in, you’ll see performance issues with -performUpdatesAnimated: completion:. The real bottleneck behind the scenes here is UICollectionView attempting to insert so many cells at once. Instead, call -reloadDataWithCompletion: when you first load data. Behind the scenes, this method does not do any diffing and simply calls -reloadData on UICollectionView. For subsequent updates, you can then use -performUpdatesAnimated: completion:.

How do I use IGListKit and estimated cell sizes with Auto Layout?

This should work in theory, and we have an example section controller, but the estimated-size API in UICollectionViewFlowLayout has changed dramatically over different iOS versions, making first-class support in IGListKit difficult. We don’t use estimated cell sizes or Auto Layout in Instagram and cannot commit to fully supporting it.

See #516 for a master list of all known issues. We very much welcome contribution to fixing this!

Is creating a “wrapper” model just for IGListKit ok?

Yes! We create models that act as a grab-bag for other models, specifically for use in section controllers. Things like:

class WeatherSectionModel {
  let location: Location
  let forecast: Forecast
  let conditions: Conditions
}

Just don’t forget to make your models diffable using the data in the contained models:

extension WeatherSectionModel: ListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return location.identifier
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    guard self !== object else { return true }
    guard let object = object as? WeatherSectionModel else { return false }
    return location == object.location && forecast == object.forecast && conditions == object.conditions
  }
}

What if I want to make my Swift structs diffable?

Give this box a try.

I want to deliver messages to certain section controllers, how do I do that?

We recommend using dependency injection and announcing changes, demonstrated in our example.

Should I reuse my section controllers between models?

No! IGListKit is designed to have a 1:1 instance mapping between objects and section controllers. IGListKit does not reuse section controllers, and if you do unintended behaviors will occur.

IGListKit does still use UICollectionView‘s cell reuse, so you shouldn’t be concerned about performance.

Why does UICollectionViewFlowLayout put everything in a new row?

UICollectionViewFlowLayout has its limitations, and it’s not well designed to support sections on the same “line”. Instead you should use IGListCollectionViewLayout.

What if I just want a section controller and don’t need the object?

Feel free to use a static string or number as your model. You can use this object as a “key” to find your section controller. Take a look at our example of this.

How do I make my cells diff and animate?

Use IGListBindingSectionController to automatically diff and animate your cells.

How can I power and update the number of items in a section controller with a dynamic array?

We recommend creating a model that owns an array to the items that power numberOfItems. Checkout our Post example that has dynamic comment cells. Just be sure to check when your array changes:

class Forecast: ListDiffable {
  let day: Date
  let hourly: [HourlyForecast]

  func diffIdentifier() -> NSObjectProtocol {
    return day
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    guard self !== object else { return true }
    guard let object = object as? Forecast else { return false }
    return hourly == object.hourly // compare elements in the arrays
  }
}
================================================ FILE: docs/css/highlight.css ================================================ /*! Jazzy - https://github.com/realm/jazzy * Copyright Realm Inc. * SPDX-License-Identifier: MIT */ /* Credit to https://gist.github.com/wataru420/2048287 */ .highlight .c { color: #999988; font-style: italic; } .highlight .err { color: #a61717; background-color: #e3d2d2; } .highlight .k { color: #000000; font-weight: bold; } .highlight .o { color: #000000; font-weight: bold; } .highlight .cm { color: #999988; font-style: italic; } .highlight .cp { color: #999999; font-weight: bold; } .highlight .c1 { color: #999988; font-style: italic; } .highlight .cs { color: #999999; font-weight: bold; font-style: italic; } .highlight .gd { color: #000000; background-color: #ffdddd; } .highlight .gd .x { color: #000000; background-color: #ffaaaa; } .highlight .ge { color: #000000; font-style: italic; } .highlight .gr { color: #aa0000; } .highlight .gh { color: #999999; } .highlight .gi { color: #000000; background-color: #ddffdd; } .highlight .gi .x { color: #000000; background-color: #aaffaa; } .highlight .go { color: #888888; } .highlight .gp { color: #555555; } .highlight .gs { font-weight: bold; } .highlight .gu { color: #aaaaaa; } .highlight .gt { color: #aa0000; } .highlight .kc { color: #000000; font-weight: bold; } .highlight .kd { color: #000000; font-weight: bold; } .highlight .kp { color: #000000; font-weight: bold; } .highlight .kr { color: #000000; font-weight: bold; } .highlight .kt { color: #445588; } .highlight .m { color: #009999; } .highlight .s { color: #d14; } .highlight .na { color: #008080; } .highlight .nb { color: #0086B3; } .highlight .nc { color: #445588; font-weight: bold; } .highlight .no { color: #008080; } .highlight .ni { color: #800080; } .highlight .ne { color: #990000; font-weight: bold; } .highlight .nf { color: #990000; } .highlight .nn { color: #555555; } .highlight .nt { color: #000080; } .highlight .nv { color: #008080; } .highlight .ow { color: #000000; font-weight: bold; } .highlight .w { color: #bbbbbb; } .highlight .mf { color: #009999; } .highlight .mh { color: #009999; } .highlight .mi { color: #009999; } .highlight .mo { color: #009999; } .highlight .sb { color: #d14; } .highlight .sc { color: #d14; } .highlight .sd { color: #d14; } .highlight .s2 { color: #d14; } .highlight .se { color: #d14; } .highlight .sh { color: #d14; } .highlight .si { color: #d14; } .highlight .sx { color: #d14; } .highlight .sr { color: #009926; } .highlight .s1 { color: #d14; } .highlight .ss { color: #990073; } .highlight .bp { color: #999999; } .highlight .vc { color: #008080; } .highlight .vg { color: #008080; } .highlight .vi { color: #008080; } .highlight .il { color: #009999; } ================================================ FILE: docs/css/jazzy.css ================================================ /*! Jazzy - https://github.com/realm/jazzy * Copyright Realm Inc. * SPDX-License-Identifier: MIT */ html, body, div, span, h1, h3, h4, p, a, code, em, img, ul, li, table, tbody, tr, td { background: transparent; border: 0; margin: 0; outline: 0; padding: 0; vertical-align: baseline; } body { background-color: #f2f2f2; font-family: Helvetica, freesans, Arial, sans-serif; font-size: 14px; -webkit-font-smoothing: subpixel-antialiased; word-wrap: break-word; } h1, h2, h3 { margin-top: 0.8em; margin-bottom: 0.3em; font-weight: 100; color: black; } h1 { font-size: 2.5em; } h2 { font-size: 2em; border-bottom: 1px solid #e2e2e2; } h4 { font-size: 13px; line-height: 1.5; margin-top: 21px; } h5 { font-size: 1.1em; } h6 { font-size: 1.1em; color: #777; } .section-name { color: gray; display: block; font-family: Helvetica; font-size: 22px; font-weight: 100; margin-bottom: 15px; } pre, code { font: 0.95em Menlo, monospace; color: #777; word-wrap: normal; } p code, li code { background-color: #eee; padding: 2px 4px; border-radius: 4px; } pre > code { padding: 0; } a { color: #0088cc; text-decoration: none; } a code { color: inherit; } ul { padding-left: 15px; } li { line-height: 1.8em; } img { max-width: 100%; } blockquote { margin-left: 0; padding: 0 10px; border-left: 4px solid #ccc; } hr { height: 1px; border: none; background-color: #e2e2e2; } .footnote-ref { display: inline-block; scroll-margin-top: 70px; } .footnote-def { scroll-margin-top: 70px; } .content-wrapper { margin: 0 auto; width: 980px; } header { font-size: 0.85em; line-height: 32px; background-color: #414141; position: fixed; width: 100%; z-index: 3; } header img { padding-right: 6px; vertical-align: -3px; height: 16px; } header a { color: #fff; } header p { float: left; color: #999; } header .header-right { float: right; margin-left: 16px; } #breadcrumbs { background-color: #f2f2f2; height: 26px; padding-top: 12px; position: fixed; width: inherit; z-index: 2; margin-top: 32px; white-space: nowrap; overflow-x: scroll; } #breadcrumbs #carat { height: 10px; margin: 0 5px; } .sidebar { background-color: #f9f9f9; border: 1px solid #e2e2e2; overflow-y: auto; overflow-x: hidden; position: fixed; top: 70px; bottom: 0; width: 230px; word-wrap: normal; } .nav-groups { list-style-type: none; background: #fff; padding-left: 0; } .nav-group-name { border-bottom: 1px solid #e2e2e2; font-size: 1.1em; font-weight: 100; padding: 15px 0 15px 20px; } .nav-group-name > a { color: #333; } .nav-group-tasks { margin-top: 5px; } .nav-group-task { font-size: 0.9em; list-style-type: none; white-space: nowrap; } .nav-group-task a { color: #888; } .main-content { background-color: #fff; border: 1px solid #e2e2e2; margin-left: 246px; position: absolute; overflow: hidden; padding-bottom: 20px; top: 70px; width: 734px; } .main-content p, .main-content a, .main-content code, .main-content em, .main-content ul, .main-content table, .main-content blockquote { margin-bottom: 1em; } .main-content p { line-height: 1.8em; } .main-content section .section:first-child { margin-top: 0; padding-top: 0; } .main-content section .task-group-section .task-group:first-of-type { padding-top: 10px; } .main-content section .task-group-section .task-group:first-of-type .section-name { padding-top: 15px; } .main-content section .heading:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .main-content .section-name p { margin-bottom: inherit; line-height: inherit; } .main-content .section-name code { background-color: inherit; padding: inherit; color: inherit; } .section { padding: 0 25px; } .highlight { background-color: #eee; padding: 10px 12px; border: 1px solid #e2e2e2; border-radius: 4px; overflow-x: auto; } .declaration .highlight { overflow-x: initial; padding: 0 40px 40px 0; margin-bottom: -25px; background-color: transparent; border: none; } .section-name { margin: 0; margin-left: 18px; } .task-group-section { margin-top: 10px; padding-left: 6px; border-top: 1px solid #e2e2e2; } .task-group { padding-top: 0px; } .task-name-container a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .section-name-container { position: relative; display: inline-block; } .section-name-container .section-name-link { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin-bottom: 0; } .section-name-container .section-name { position: relative; pointer-events: none; z-index: 1; } .section-name-container .section-name a { pointer-events: auto; } .item { padding-top: 8px; width: 100%; list-style-type: none; } .item a[name]:before { content: ""; display: block; padding-top: 70px; margin: -70px 0 0; } .item code { background-color: transparent; padding: 0; } .item .token, .item .direct-link { display: inline-block; text-indent: -20px; padding-left: 3px; margin-left: 35px; font-size: 11.9px; transition: all 300ms; } .item .token-open { margin-left: 20px; } .item .discouraged { text-decoration: line-through; } .declaration-note { font-size: .85em; color: gray; font-style: italic; } .pointer-container { border-bottom: 1px solid #e2e2e2; left: -23px; padding-bottom: 13px; position: relative; width: 110%; } .pointer { background: #f9f9f9; border-left: 1px solid #e2e2e2; border-top: 1px solid #e2e2e2; height: 12px; left: 21px; top: -7px; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); position: absolute; width: 12px; } .height-container { display: none; left: -25px; padding: 0 25px; position: relative; width: 100%; overflow: hidden; } .height-container .section { background: #f9f9f9; border-bottom: 1px solid #e2e2e2; left: -25px; position: relative; width: 100%; padding-top: 10px; padding-bottom: 5px; } .aside, .language { padding: 6px 12px; margin: 12px 0; border-left: 5px solid #dddddd; overflow-y: hidden; } .aside .aside-title, .language .aside-title { font-size: 9px; letter-spacing: 2px; text-transform: uppercase; padding-bottom: 0; margin: 0; color: #aaa; -webkit-user-select: none; } .aside p:last-child, .language p:last-child { margin-bottom: 0; } .language { border-left: 5px solid #cde9f4; } .language .aside-title { color: #4b8afb; } .aside-warning, .aside-deprecated, .aside-unavailable { border-left: 5px solid #ff6666; } .aside-warning .aside-title, .aside-deprecated .aside-title, .aside-unavailable .aside-title { color: #ff0000; } .graybox { border-collapse: collapse; width: 100%; } .graybox p { margin: 0; word-break: break-word; min-width: 50px; } .graybox td { border: 1px solid #e2e2e2; padding: 5px 25px 5px 10px; vertical-align: middle; } .graybox tr td:first-of-type { text-align: right; padding: 7px; vertical-align: top; word-break: normal; width: 40px; } .slightly-smaller { font-size: 0.9em; } #footer { position: relative; top: 10px; bottom: 0px; margin-left: 25px; } #footer p { margin: 0; color: #aaa; font-size: 0.8em; } html.dash header, html.dash #breadcrumbs, html.dash .sidebar { display: none; } html.dash .main-content { width: 980px; margin-left: 0; border: none; width: 100%; top: 0; padding-bottom: 0; } html.dash .height-container { display: block; } html.dash .item .token { margin-left: 0; } html.dash .content-wrapper { width: auto; } html.dash #footer { position: static; } form[role=search] { float: right; } form[role=search] input { font: Helvetica, freesans, Arial, sans-serif; margin-top: 6px; font-size: 13px; line-height: 20px; padding: 0px 10px; border: none; border-radius: 1em; } .loading form[role=search] input { background: white url(../img/spinner.gif) center right 4px no-repeat; } form[role=search] .tt-menu { margin: 0; min-width: 300px; background: #fff; color: #333; border: 1px solid #e2e2e2; z-index: 4; } form[role=search] .tt-highlight { font-weight: bold; } form[role=search] .tt-suggestion { font: Helvetica, freesans, Arial, sans-serif; font-size: 14px; padding: 0 8px; } form[role=search] .tt-suggestion span { display: table-cell; white-space: nowrap; } form[role=search] .tt-suggestion .doc-parent-name { width: 100%; text-align: right; font-weight: normal; font-size: 0.9em; padding-left: 16px; } form[role=search] .tt-suggestion:hover, form[role=search] .tt-suggestion.tt-cursor { cursor: pointer; background-color: #4183c4; color: #fff; } form[role=search] .tt-suggestion:hover .doc-parent-name, form[role=search] .tt-suggestion.tt-cursor .doc-parent-name { color: #fff; } ================================================ FILE: docs/generating-your-models-using-remodel.html ================================================ Generating your models using remodel Reference

Generating your IGListDiffable models using remodel

With the IGListDiffable plugin for remodel by facebook, you can automatically generate models conforming to the IGListDiffable.

This will automatically implement hash, isEqual: and description, as well as diffIdentifier and isEqualToDiffableObject: for you. Remodel is also capable to generate additional code, like conforming to NSCoding or additional Builder classes for your model object. It will make creating and updating models much easier, faster and safer.

In /remodel-plugin, you can find the source files to build the IGListDiffable plugin locally.

Installation

1. Remodel installation

Please follow the installation instructions in the main remodel repository.

tl;dr: Either clone the original repository, or use an npm installation. In the latter case you can run which remodel-gen to find out the path of your installation.

2. Plugin installation

Copy the following files & folders within /remodel-plugin into your local remodel checkout:

  • /src/plugins/iglistdiffable.ts - the actual plugin
  • /src/__tests__/plugins/iglistdiffable-test.ts - unit tests
  • /features/iglistdiffable.feature - integration tests

And then register the new plugin with the system:

  • Edit /remodel/src/value-object-default-config.ts and add iglistdiffable to the list of basePlugins:
// value-object-default-config.ts
basePlugins: List.of(
    'assert-nullability',
    'assume-nonnull',
    'builder',
    'coding',
    'copying',
    'description',
    'equality',
    'fetch-status',
    'immutable-properties',
    'init-new-unavailable',
    'use-cpp',
    'iglistdiffable'
  )

3. Build plugin:

Once you copied them over and registered the plugin, you have to compile the typescript files into javascript. Do do so run this command from the remodel directory:

  • ./bin/build

4. Run tests (optional)

To run the unit/integration tests, you can run the following commands:

  • ./bin/runUnitTests
  • ./bin/runAcceptanceTests

This is especially useful if you plan to change/extend the plugin in any way.

5. Use the plugin

Now you are ready to generate your IGListDiffable conforming models! To generate a model, create a new .value file. Here’s an example:

# PersonModel.value
PersonModel includes(IGListDiffable) {
  NSString *firstName
  NSString *lastName
  %diffIdentifier
  NSString *uniqueId
}

To generate your Objective-C models, run the generation tool like this:

./bin/generate path/to/your/PersonModel.value

This will generate the following Objective-C files in the same directory:

// PersonModel.h
@interface PersonModel : NSObject <IGListDiffable, NSCopying>

@property (nonatomic, readonly, copy) NSString *firstName;
@property (nonatomic, readonly, copy) NSString *lastName;
@property (nonatomic, readonly, copy) NSString *uniqueId;

- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId;

@end

and

// PersonModel.m
@implementation PersonModel

- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName uniqueId:(NSString *)uniqueId
{
  if ((self = [super init])) {
    _firstName = [firstName copy];
    _lastName = [lastName copy];
    _uniqueId = [uniqueId copy];
  }

  return self;
}

- (id<NSObject>)diffIdentifier
{
  return _uniqueId;
}

- (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object
{
  return [self isEqual:object];
}

- (BOOL)isEqual:(PersonModel *)object
{
  if (self == object) {
    return YES;
  } else if (self == nil || object == nil || ![object isKindOfClass:[self class]]) {
    return NO;
  }
  return
    (_firstName == object->_firstName ? YES : [_firstName isEqual:object->_firstName]) &&
    (_lastName == object->_lastName ? YES : [_lastName isEqual:object->_lastName]) &&
    (_uniqueId == object->_uniqueId ? YES : [_uniqueId isEqual:object->_uniqueId]);
}

- (id)copyWithZone:(nullable NSZone *)zone
{
  return self;
}

- (NSString *)description
{
  return [NSString stringWithFormat:@"%@ - \n\t firstName: %@; \n\t lastName: %@; \n\t uniqueId: %@; \n", [super description], _firstName, _lastName, _uniqueId];
}

- (NSUInteger)hash
{
  NSUInteger subhashes[] = {[_firstName hash], [_lastName hash], [_uniqueId hash]};
  NSUInteger result = subhashes[0];
  for (int ii = 1; ii < 3; ++ii) {
    unsigned long long base = (((unsigned long long)result) << 32 | subhashes[ii]);
    base = (~base) + (base << 18);
    base ^= (base >> 31);
    base *=  21;
    base ^= (base >> 11);
    base += (base << 6);
    base ^= (base >> 22);
    result = base;
  }
  return result;
}

@end

Documentation

Please see the main remodel repository for additional documentation

================================================ FILE: docs/getting-started.html ================================================ Getting Started Reference

Getting Started

This guide provides a brief overview for how to get started using IGListKit.

Creating your first list

After installing IGListKit, creating a new list is easy.

Creating a section controller

Creating a new section controller is simple. Subclass IGListSectionController and override at least cellForItemAtIndex: and sizeForItemAtIndex:.

Take a look at LabelSectionController for an example section controller that handles a String and configures a single cell with a UILabel.

class LabelSectionController: ListSectionController {
  override func sizeForItem(at index: Int) -> CGSize {
    return CGSize(width: collectionContext!.containerSize.width, height: 55)
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    return collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index)
  }
}

Creating the UI

After creating at least one section controller, you must create a UICollectionView and IGListAdapter.

let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)

let updater = ListAdapterUpdater()
let adapter = ListAdapter(updater: updater, viewController: self)
adapter.collectionView = collectionView

Note: This example is done within a UIViewController and uses both a stock UICollectionViewFlowLayout and IGListAdapterUpdater. You can use your own layout and updater if you need advanced features!

Connecting the data source

The last step is the IGListAdapter‘s data source and returning some data.

func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
  // this can be anything!
  return [ "Foo", "Bar", 42, "Biz" ]
}

func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
  if object is String {
    return LabelSectionController()
  } else {
    return NumberSectionController()
  }
}

func emptyView(for listAdapter: ListAdapter) -> UIView? {
  return nil
}

After you have created the data source you need to connect it to the IGListAdapter by setting its dataSource property:

adapter.dataSource = self

You can return an array of any type of data, as long as it conforms to IGListDiffable.

Immutability

The data should be immutable. If you return mutable objects that you will be editing later, IGListKit will not be able to diff the models accurately. This is because the instances have already been changed. Thus, the updates to the objects would be lost. Instead, always return a newly instantiated, immutable object and implement IGListDiffable.

Diffing

IGListKit uses an algorithm adapted from a paper titled A technique for isolating differences between files by Paul Heckel. This algorithm uses a technique known as the longest common subsequence to find a minimal diff between collections in linear time O(n). It finds all inserts, deletes, updates, and moves between arrays of data.

To add custom, diffable models, you need to conform to the IGListDiffable protocol and implement diffIdentifier() and isEqual(toDiffableObject:).

Note: an object’s diffIdentifier() should never change. If an object mutates it’s diffIdentifer() the behavior of IGListKit is undefined (and almost assuredly undesirable).

For an example, consider the following model:

class User {
  let primaryKey: Int
  let name: String
  // implementation, etc
}

The user’s primaryKey uniquely identifies user data, and the name is just the value for that user.

Let’s say a server returns a User object that looks like this:

let shayne = User(primaryKey: 2, name: "Shayne")

But sometime after the client receives shayne, someone changes their name:

let ann = User(primaryKey: 2, name: "Ann")

Both shayne and ann represent the same unique data because they share the same primaryKey, but they are not equal because their names are different.

To represent this in IGListKit’s diffing, add and implement the IGListDiffable protocol:

extension User: ListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return primaryKey
  }

  func isEqual(toDiffableObject object: Any?) -> Bool {
    if let object = object as? User {
      return name == object.name
    }
    return false
  }
}

The algorithm will skip updating two User objects that have the same primaryKey and name, even if they are different instances! You now avoid unnecessary UI updates in the collection view even when providing new instances.

Note: Remember that isEqual(toDiffableObject:) should return false when you want to reload the cells in the corresponding section controller.

Diffing outside of IGListKit

If you want to use the diffing algorithm outside of IGListAdapter and UICollectionView, you can! The diffing algorithm was built with the flexibility to be used with any models that conform to IGListDiffable.

let result = ListDiff(oldArray: oldUsers, newArray: newUsers, .equality)

With this you have all of the deletes, reloads, moves, and inserts! There’s even a function to generate NSIndexPath results.

Advanced Features

Working Range

A working range is a range of section controllers who aren’t yet visible, but are near the screen. Section controllers are notified of their entrance and exit to this range. This concept lets your section controllers prepare content before they come on screen (e.g. download images).

The IGListAdapter must be initialized with a range value in order to work. This value is a multiple of the visible height or width, depending on the scroll-direction.

let adapter = ListAdapter(updater: ListAdapterUpdater(),
                   viewController: self,
                 workingRangeSize: 1) // 1 before/after visible objects

working-range

You can set the weak workingRangeDelegate on a section controller to receive events.

Supplementary Views

Adding supplementary views to section controllers is as simple as setting the (weak) supplementaryViewSource and implementing the IGListSupplementaryViewSource protocol. This protocol works nearly the same as returning and configuring cells.

Display Delegate

Section controllers can set the weak displayDelegate delegate to an object, including self, to receive display events about a section controller and individual cells.

Custom Updaters

The default IGListAdapterUpdater should handle any UICollectionView update that you need. However, if you find the functionality lacking, or want to perform updates in a very specific way, you can create an object that conforms to the IGListUpdatingDelegate protocol and initialize a new IGListAdapter with it.

Check out the updater IGListReloadDataUpdater (used in unit tests) for an example.

================================================ FILE: docs/iglistdiffable-and-equality.html ================================================ IGListDiffable and Equality Reference

IGListDiffable and Equality

This guide explains the IGListDiffable protocol and how to write good -isEqual: methods.

Background

The IGListDiffable protocol requires clients to implement two methods, -diffIdentifier and -isEqualToDiffableObject:.

The method -isEqualToDiffableObject: should perform the same type of check as -isEqual:, but without impacting performance characteristics, like in Objective-C containers such as NSDictionary and NSSet.

Why are both of these methods required for diffing? The point of having the two methods has to do with identity and equality, where the diff identifier uniquely identifies data (common scenario is primary key in databases). Equality comes into play when comparing the values of two uniquely identical objects (driving reloading).

See also: #509

IGListDiffable bare minimum

The quickest way to get started with diffable models is use the object itself as the identifier, and use the superclass’s -[NSObject isEqual:] implementation for equality:

- (id<NSObject>)diffIdentifier {
  return self;
}

- (BOOL)isEqualToDiffableObject:(id<IGListDiffable>)object {
  return [self isEqual:object];
}

Writing better Equality methods

Even though IGListKit uses the method -isEqualToDiffableObject:, the concepts of writing a good equality check apply in general. Here are the basics to writing good -isEqual: and -hash functions. Note this is all Objective-C but applies to Swift also.

  • If you override -isEqual: you must override -hash. Check out this article by Mike Ash for details.
  • Always compare the pointer first. This saves a lot of wasteful objc_msgSend(...) calls and value comparisons if checking the same instance.
  • When comparing object values, always check for nil before -isEqual:. For example, [nil isEqual:nil] counterintuitively returns NO. Instead, do left == right || [left isEqual:right].
  • Always compare the cheapest values first. For example, doing [self.array isEqual:other.array] && self.intVal == other.intVal is extremely wasteful if the intVal values are different. Use lazy evaluation!

As an example, if I had a User model with the following interface:

@interface User : NSObject

@property NSInteger identifier;
@property NSString *name;
@property NSArray *posts;

@end

You would implement its equality methods like so:

@implementation User

- (NSUInteger)hash {
  return self.identifier;
}

- (BOOL)isEqual:(id)object {
  if (self == object) { 
      return YES;
  }

  if (![object isKindOfClass:[User class]]) {
      return NO;
  }

  User *right = object;
  return self.identifier == right.identifier 
      && (self.name == right.name || [self.name isEqual:right.name])
      && (self.posts == right.posts || [self.posts isEqualToArray:right.posts]);
}

@end

Using both IGListDiffable and -isEqual:

Making your objects work universally with Objective-C containers and IGListKit is easy once you’ve implemented -isEqual: and -hash.

@interface User <IGListDiffable>

// properties...

@end

@implementation User

- (id<NSObject>)diffIdentifier {
    return @(self.identifier);
}

- (BOOL)isEqualToDiffableObject:(id<IGListDiffable>)object {
    return [self isEqual:object];
}

@end
================================================ FILE: docs/index.html ================================================ IGListKit Reference

Build Status Coverage Status Pods Version Platforms Carthage Compatible


A data-driven UICollectionView framework for building fast and flexible lists.

Main Features
🙅 Never call performBatchUpdates(_:, completion:) or reloadData() again
🏠 Better architecture with reusable cells and components
🔠 Create collections with multiple data types
🔑 Decoupled diffing algorithm
Fully unit tested
🔍 Customize your diffing behavior for your models
📱 Simply UICollectionView at its core
🚀 Extendable API
🐦 Written in Objective-C with full Swift interop support

IGListKit is built and maintained with ❤️ by Instagram engineering. We use the open source version main branch in the Instagram app.

Multilingual translation

Chinese README

Requirements

  • Swift 5.1+
  • iOS 11.0+
  • tvOS 11.0+
  • macOS 10.13+ (diffing algorithm components only)
  • Interoperability with Swift 3.0+

Installation

CocoaPods

The preferred installation method is with CocoaPods. Add the following to your Podfile:

pod 'IGListKit', '~> 5.2.0'

Carthage

For Carthage, add the following to your Cartfile:

github "Instagram/IGListKit" ~> 5.2.0

Swift Package Manager

For Swift Package Manager:

To integrate using Xcode:

File -> Swift Packages -> Add Package Dependency

Enter package URL: https://github.com/Instagram/IGListKit, and select the latest release.

For advanced usage, see our Installation Guide.

Getting Started

Try out IGListKit by opening any of the sample apps available in the Examples directory.

Documentation

You can find the docs here. Documentation is generated with jazzy and hosted on GitHub-Pages.

To regenerate docs, run ./scripts/build_docs.sh from the root directory in the repo.

Vision

For the long-term goals and “vision” of IGListKit, please read our Vision doc.

Contributing

Please see the CONTRIBUTING file for how to help. At Instagram, we sync the open source version of IGListKit daily, so we’re always testing the latest changes. But that requires all changes be thoroughly tested and follow our style guide.

We have a set of starter tasks that are great for beginners to jump in on and start contributing.

License

IGListKit is MIT-licensed.

The files in the /Examples/ directory are licensed under a separate license as specified in each file. Documentation is licensed CC-BY-4.0.

Copyright © Meta Platforms, Inc • Terms of UsePrivacy Policy

================================================ FILE: docs/installation.html ================================================ Installation Reference

Installation

This guide provides details on how to install IGListKit.

CocoaPods

The preferred method of installation for IGListKit is using CocoaPods.

In order to use the latest release of the framework, add the following to your Podfile:

pod 'IGListKit', '~> 4.0'

Using main

Alternatively, you can use the latest version from the main branch. This is what we use at Instagram, so you can be confident that main is always stable and reliable.

pod 'IGListKit', :git => 'https://github.com/Instagram/IGListKit.git', :branch => 'main'

Note: while main is stable, it may have breaking changes. Before updating to main, be sure to check the CHANGELOG for details on changes.

Subspecs

With the exception of macOS (which currently only supports the diffing algorithm components), using pod 'IGListKit' will get you the full library, including the flexible UICollectionView system. Learn more about how to get started in our Getting Started guide.

However, if you only want to use the diffing components of this library, then you can use the diffing subspec in your Podfile:

pod 'IGListKit/Diffing', '~> 4.0'

Regardless of whether you only use the diffing components, or the entire library, the imports are the same:

import IGListKit

Carthage

If using Carthage, add the following to your Cartfile:

github "Instagram/IGListKit" ~> 4.0
================================================ FILE: docs/js/jazzy.js ================================================ // Jazzy - https://github.com/realm/jazzy // Copyright Realm Inc. // SPDX-License-Identifier: MIT window.jazzy = {'docset': false} if (typeof window.dash != 'undefined') { document.documentElement.className += ' dash' window.jazzy.docset = true } if (navigator.userAgent.match(/xcode/i)) { document.documentElement.className += ' xcode' window.jazzy.docset = true } function toggleItem($link, $content) { var animationDuration = 300; $link.toggleClass('token-open'); $content.slideToggle(animationDuration); } function itemLinkToContent($link) { return $link.parent().parent().next(); } // On doc load + hash-change, open any targeted item function openCurrentItemIfClosed() { if (window.jazzy.docset) { return; } var $link = $(`a[name="${location.hash.substring(1)}"]`).nextAll('.token'); $content = itemLinkToContent($link); if ($content.is(':hidden')) { toggleItem($link, $content); } } $(openCurrentItemIfClosed); $(window).on('hashchange', openCurrentItemIfClosed); // On item link ('token') click, toggle its discussion $('.token').on('click', function(event) { if (window.jazzy.docset) { return; } var $link = $(this); toggleItem($link, itemLinkToContent($link)); // Keeps the document from jumping to the hash. var href = $link.attr('href'); if (history.pushState) { history.pushState({}, '', href); } else { location.hash = href; } event.preventDefault(); }); // Clicks on links to the current, closed, item need to open the item $("a:not('.token')").on('click', function() { if (location == this.href) { openCurrentItemIfClosed(); } }); // KaTeX rendering if ("katex" in window) { $($('.math').each( (_, element) => { katex.render(element.textContent, element, { displayMode: $(element).hasClass('m-block'), throwOnError: false, trust: true }); })) } ================================================ FILE: docs/js/jazzy.search.js ================================================ // Jazzy - https://github.com/realm/jazzy // Copyright Realm Inc. // SPDX-License-Identifier: MIT $(function(){ var $typeahead = $('[data-typeahead]'); var $form = $typeahead.parents('form'); var searchURL = $form.attr('action'); function displayTemplate(result) { return result.name; } function suggestionTemplate(result) { var t = '
'; t += '' + result.name + ''; if (result.parent_name) { t += '' + result.parent_name + ''; } t += '
'; return t; } $typeahead.one('focus', function() { $form.addClass('loading'); $.getJSON(searchURL).then(function(searchData) { const searchIndex = lunr(function() { this.ref('url'); this.field('name'); this.field('abstract'); for (const [url, doc] of Object.entries(searchData)) { this.add({url: url, name: doc.name, abstract: doc.abstract}); } }); $typeahead.typeahead( { highlight: true, minLength: 3, autoselect: true }, { limit: 10, display: displayTemplate, templates: { suggestion: suggestionTemplate }, source: function(query, sync) { const lcSearch = query.toLowerCase(); const results = searchIndex.query(function(q) { q.term(lcSearch, { boost: 100 }); q.term(lcSearch, { boost: 10, wildcard: lunr.Query.wildcard.TRAILING }); }).map(function(result) { var doc = searchData[result.ref]; doc.url = result.ref; return doc; }); sync(results); } } ); $form.removeClass('loading'); $typeahead.trigger('focus'); }); }); var baseURL = searchURL.slice(0, -"search.json".length); $typeahead.on('typeahead:select', function(e, result) { window.location = baseURL + result.url; }); }); ================================================ FILE: docs/js/typeahead.jquery.js ================================================ /*! * typeahead.js 1.3.3 * https://github.com/corejavascript/typeahead.js * Copyright 2013-2024 Twitter, Inc. and other contributors; Licensed MIT */ (function(root, factory) { if (typeof define === "function" && define.amd) { define([ "jquery" ], function(a0) { return factory(a0); }); } else if (typeof module === "object" && module.exports) { module.exports = factory(require("jquery")); } else { factory(root["jQuery"]); } })(this, function($) { var _ = function() { "use strict"; return { isMsie: function() { return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; }, isBlankString: function(str) { return !str || /^\s*$/.test(str); }, escapeRegExChars: function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); }, isString: function(obj) { return typeof obj === "string"; }, isNumber: function(obj) { return typeof obj === "number"; }, isArray: $.isArray, isFunction: $.isFunction, isObject: $.isPlainObject, isUndefined: function(obj) { return typeof obj === "undefined"; }, isElement: function(obj) { return !!(obj && obj.nodeType === 1); }, isJQuery: function(obj) { return obj instanceof $; }, toStr: function toStr(s) { return _.isUndefined(s) || s === null ? "" : s + ""; }, bind: $.proxy, each: function(collection, cb) { $.each(collection, reverseArgs); function reverseArgs(index, value) { return cb(value, index); } }, map: $.map, filter: $.grep, every: function(obj, test) { var result = true; if (!obj) { return result; } $.each(obj, function(key, val) { if (!(result = test.call(null, val, key, obj))) { return false; } }); return !!result; }, some: function(obj, test) { var result = false; if (!obj) { return result; } $.each(obj, function(key, val) { if (result = test.call(null, val, key, obj)) { return false; } }); return !!result; }, mixin: $.extend, identity: function(x) { return x; }, clone: function(obj) { return $.extend(true, {}, obj); }, getIdGenerator: function() { var counter = 0; return function() { return counter++; }; }, templatify: function templatify(obj) { return $.isFunction(obj) ? obj : template; function template() { return String(obj); } }, defer: function(fn) { setTimeout(fn, 0); }, debounce: function(func, wait, immediate) { var timeout, result; return function() { var context = this, args = arguments, later, callNow; later = function() { timeout = null; if (!immediate) { result = func.apply(context, args); } }; callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); } return result; }; }, throttle: function(func, wait) { var context, args, timeout, result, previous, later; previous = 0; later = function() { previous = new Date(); timeout = null; result = func.apply(context, args); }; return function() { var now = new Date(), remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0) { clearTimeout(timeout); timeout = null; previous = now; result = func.apply(context, args); } else if (!timeout) { timeout = setTimeout(later, remaining); } return result; }; }, stringify: function(val) { return _.isString(val) ? val : JSON.stringify(val); }, guid: function() { function _p8(s) { var p = (Math.random().toString(16) + "000000000").substr(2, 8); return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p; } return "tt-" + _p8() + _p8(true) + _p8(true) + _p8(); }, noop: function() {} }; }(); var WWW = function() { "use strict"; var defaultClassNames = { wrapper: "twitter-typeahead", input: "tt-input", hint: "tt-hint", menu: "tt-menu", dataset: "tt-dataset", suggestion: "tt-suggestion", selectable: "tt-selectable", empty: "tt-empty", open: "tt-open", cursor: "tt-cursor", highlight: "tt-highlight" }; return build; function build(o) { var www, classes; classes = _.mixin({}, defaultClassNames, o); www = { css: buildCss(), classes: classes, html: buildHtml(classes), selectors: buildSelectors(classes) }; return { css: www.css, html: www.html, classes: www.classes, selectors: www.selectors, mixin: function(o) { _.mixin(o, www); } }; } function buildHtml(c) { return { wrapper: '', menu: '
' }; } function buildSelectors(classes) { var selectors = {}; _.each(classes, function(v, k) { selectors[k] = "." + v; }); return selectors; } function buildCss() { var css = { wrapper: { position: "relative", display: "inline-block" }, hint: { position: "absolute", top: "0", left: "0", borderColor: "transparent", boxShadow: "none", opacity: "1" }, input: { position: "relative", verticalAlign: "top", backgroundColor: "transparent" }, inputWithNoHint: { position: "relative", verticalAlign: "top" }, menu: { position: "absolute", top: "100%", left: "0", zIndex: "100", display: "none" }, ltr: { left: "0", right: "auto" }, rtl: { left: "auto", right: " 0" } }; if (_.isMsie()) { _.mixin(css.input, { backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" }); } return css; } }(); var EventBus = function() { "use strict"; var namespace, deprecationMap; namespace = "typeahead:"; deprecationMap = { render: "rendered", cursorchange: "cursorchanged", select: "selected", autocomplete: "autocompleted" }; function EventBus(o) { if (!o || !o.el) { $.error("EventBus initialized without el"); } this.$el = $(o.el); } _.mixin(EventBus.prototype, { _trigger: function(type, args) { var $e = $.Event(namespace + type); this.$el.trigger.call(this.$el, $e, args || []); return $e; }, before: function(type) { var args, $e; args = [].slice.call(arguments, 1); $e = this._trigger("before" + type, args); return $e.isDefaultPrevented(); }, trigger: function(type) { var deprecatedType; this._trigger(type, [].slice.call(arguments, 1)); if (deprecatedType = deprecationMap[type]) { this._trigger(deprecatedType, [].slice.call(arguments, 1)); } } }); return EventBus; }(); var EventEmitter = function() { "use strict"; var splitter = /\s+/, nextTick = getNextTick(); return { onSync: onSync, onAsync: onAsync, off: off, trigger: trigger }; function on(method, types, cb, context) { var type; if (!cb) { return this; } types = types.split(splitter); cb = context ? bindContext(cb, context) : cb; this._callbacks = this._callbacks || {}; while (type = types.shift()) { this._callbacks[type] = this._callbacks[type] || { sync: [], async: [] }; this._callbacks[type][method].push(cb); } return this; } function onAsync(types, cb, context) { return on.call(this, "async", types, cb, context); } function onSync(types, cb, context) { return on.call(this, "sync", types, cb, context); } function off(types) { var type; if (!this._callbacks) { return this; } types = types.split(splitter); while (type = types.shift()) { delete this._callbacks[type]; } return this; } function trigger(types) { var type, callbacks, args, syncFlush, asyncFlush; if (!this._callbacks) { return this; } types = types.split(splitter); args = [].slice.call(arguments, 1); while ((type = types.shift()) && (callbacks = this._callbacks[type])) { syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); syncFlush() && nextTick(asyncFlush); } return this; } function getFlush(callbacks, context, args) { return flush; function flush() { var cancelled; for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { cancelled = callbacks[i].apply(context, args) === false; } return !cancelled; } } function getNextTick() { var nextTickFn; if (window.setImmediate) { nextTickFn = function nextTickSetImmediate(fn) { setImmediate(function() { fn(); }); }; } else { nextTickFn = function nextTickSetTimeout(fn) { setTimeout(function() { fn(); }, 0); }; } return nextTickFn; } function bindContext(fn, context) { return fn.bind ? fn.bind(context) : function() { fn.apply(context, [].slice.call(arguments, 0)); }; } }(); var highlight = function(doc) { "use strict"; var defaults = { node: null, pattern: null, tagName: "strong", className: null, wordsOnly: false, caseSensitive: false, diacriticInsensitive: false }; var accented = { A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]", B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]", C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]", D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]", E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]", F: "[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]", G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]", H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]", I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]", J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]", K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]", L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]", M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]", N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]", O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]", P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]", Q: "[Qqℚ⒬Ⓠⓠ㏃Qq]", R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]", S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]", T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]", U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]", V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]", W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]", X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]", Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]", Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]" }; return function hightlight(o) { var regex; o = _.mixin({}, defaults, o); if (!o.node || !o.pattern) { return; } o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive); traverse(o.node, hightlightTextNode); function hightlightTextNode(textNode) { var match, patternNode, wrapperNode; if (match = regex.exec(textNode.data)) { wrapperNode = doc.createElement(o.tagName); o.className && (wrapperNode.className = o.className); patternNode = textNode.splitText(match.index); patternNode.splitText(match[0].length); wrapperNode.appendChild(patternNode.cloneNode(true)); textNode.parentNode.replaceChild(wrapperNode, patternNode); } return !!match; } function traverse(el, hightlightTextNode) { var childNode, TEXT_NODE_TYPE = 3; for (var i = 0; i < el.childNodes.length; i++) { childNode = el.childNodes[i]; if (childNode.nodeType === TEXT_NODE_TYPE) { i += hightlightTextNode(childNode) ? 1 : 0; } else { traverse(childNode, hightlightTextNode); } } } }; function accent_replacer(chr) { return accented[chr.toUpperCase()] || chr; } function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) { var escapedPatterns = [], regexStr; for (var i = 0, len = patterns.length; i < len; i++) { var escapedWord = _.escapeRegExChars(patterns[i]); if (diacriticInsensitive) { escapedWord = escapedWord.replace(/\S/g, accent_replacer); } escapedPatterns.push(escapedWord); } regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); } }(window.document); var Input = function() { "use strict"; var specialKeyCodeMap; specialKeyCodeMap = { 9: "tab", 27: "esc", 37: "left", 39: "right", 13: "enter", 38: "up", 40: "down" }; function Input(o, www) { var id; o = o || {}; if (!o.input) { $.error("input is missing"); } www.mixin(this); this.$hint = $(o.hint); this.$input = $(o.input); this.$menu = $(o.menu); id = this.$input.attr("id") || _.guid(); this.$menu.attr("id", id + "_listbox"); this.$hint.attr({ "aria-hidden": true }); this.$input.attr({ "aria-owns": id + "_listbox", "aria-controls": id + "_listbox", role: "combobox", "aria-autocomplete": "list", "aria-expanded": false }); this.query = this.$input.val(); this.queryWhenFocused = this.hasFocus() ? this.query : null; this.$overflowHelper = buildOverflowHelper(this.$input); this._checkLanguageDirection(); if (this.$hint.length === 0) { this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; } this.onSync("cursorchange", this._updateDescendent); } Input.normalizeQuery = function(str) { return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); }; _.mixin(Input.prototype, EventEmitter, { _onBlur: function onBlur() { this.resetInputValue(); this.trigger("blurred"); }, _onFocus: function onFocus() { this.queryWhenFocused = this.query; this.trigger("focused"); }, _onKeydown: function onKeydown($e) { var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; this._managePreventDefault(keyName, $e); if (keyName && this._shouldTrigger(keyName, $e)) { this.trigger(keyName + "Keyed", $e); } }, _onInput: function onInput() { this._setQuery(this.getInputValue()); this.clearHintIfInvalid(); this._checkLanguageDirection(); }, _managePreventDefault: function managePreventDefault(keyName, $e) { var preventDefault; switch (keyName) { case "up": case "down": preventDefault = !withModifier($e); break; default: preventDefault = false; } preventDefault && $e.preventDefault(); }, _shouldTrigger: function shouldTrigger(keyName, $e) { var trigger; switch (keyName) { case "tab": trigger = !withModifier($e); break; default: trigger = true; } return trigger; }, _checkLanguageDirection: function checkLanguageDirection() { var dir = (this.$input.css("direction") || "ltr").toLowerCase(); if (this.dir !== dir) { this.dir = dir; this.$hint.attr("dir", dir); this.trigger("langDirChanged", dir); } }, _setQuery: function setQuery(val, silent) { var areEquivalent, hasDifferentWhitespace; areEquivalent = areQueriesEquivalent(val, this.query); hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; this.query = val; if (!silent && !areEquivalent) { this.trigger("queryChanged", this.query); } else if (!silent && hasDifferentWhitespace) { this.trigger("whitespaceChanged", this.query); } }, _updateDescendent: function updateDescendent(event, id) { this.$input.attr("aria-activedescendant", id); }, bind: function() { var that = this, onBlur, onFocus, onKeydown, onInput; onBlur = _.bind(this._onBlur, this); onFocus = _.bind(this._onFocus, this); onKeydown = _.bind(this._onKeydown, this); onInput = _.bind(this._onInput, this); this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); if (!_.isMsie() || _.isMsie() > 9) { this.$input.on("input.tt", onInput); } else { this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { if (specialKeyCodeMap[$e.which || $e.keyCode]) { return; } _.defer(_.bind(that._onInput, that, $e)); }); } return this; }, focus: function focus() { this.$input.focus(); }, blur: function blur() { this.$input.blur(); }, getLangDir: function getLangDir() { return this.dir; }, getQuery: function getQuery() { return this.query || ""; }, setQuery: function setQuery(val, silent) { this.setInputValue(val); this._setQuery(val, silent); }, hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { return this.query !== this.queryWhenFocused; }, getInputValue: function getInputValue() { return this.$input.val(); }, setInputValue: function setInputValue(value) { this.$input.val(value); this.clearHintIfInvalid(); this._checkLanguageDirection(); }, resetInputValue: function resetInputValue() { this.setInputValue(this.query); }, getHint: function getHint() { return this.$hint.val(); }, setHint: function setHint(value) { this.$hint.val(value); }, clearHint: function clearHint() { this.setHint(""); }, clearHintIfInvalid: function clearHintIfInvalid() { var val, hint, valIsPrefixOfHint, isValid; val = this.getInputValue(); hint = this.getHint(); valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); !isValid && this.clearHint(); }, hasFocus: function hasFocus() { return this.$input.is(":focus"); }, hasOverflow: function hasOverflow() { var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { var valueLength, selectionStart, range; valueLength = this.$input.val().length; selectionStart = this.$input[0].selectionStart; if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { range = document.selection.createRange(); range.moveStart("character", -valueLength); return valueLength === range.text.length; } return true; }, destroy: function destroy() { this.$hint.off(".tt"); this.$input.off(".tt"); this.$overflowHelper.remove(); this.$hint = this.$input = this.$overflowHelper = $("
"); }, setAriaExpanded: function setAriaExpanded(value) { this.$input.attr("aria-expanded", value); } }); return Input; function buildOverflowHelper($input) { return $('').css({ position: "absolute", visibility: "hidden", whiteSpace: "pre", fontFamily: $input.css("font-family"), fontSize: $input.css("font-size"), fontStyle: $input.css("font-style"), fontVariant: $input.css("font-variant"), fontWeight: $input.css("font-weight"), wordSpacing: $input.css("word-spacing"), letterSpacing: $input.css("letter-spacing"), textIndent: $input.css("text-indent"), textRendering: $input.css("text-rendering"), textTransform: $input.css("text-transform") }).insertAfter($input); } function areQueriesEquivalent(a, b) { return Input.normalizeQuery(a) === Input.normalizeQuery(b); } function withModifier($e) { return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; } }(); var Dataset = function() { "use strict"; var keys, nameGenerator; keys = { dataset: "tt-selectable-dataset", val: "tt-selectable-display", obj: "tt-selectable-object" }; nameGenerator = _.getIdGenerator(); function Dataset(o, www) { o = o || {}; o.templates = o.templates || {}; o.templates.notFound = o.templates.notFound || o.templates.empty; if (!o.source) { $.error("missing source"); } if (!o.node) { $.error("missing node"); } if (o.name && !isValidName(o.name)) { $.error("invalid dataset name: " + o.name); } www.mixin(this); this.highlight = !!o.highlight; this.name = _.toStr(o.name || nameGenerator()); this.limit = o.limit || 5; this.displayFn = getDisplayFn(o.display || o.displayKey); this.templates = getTemplates(o.templates, this.displayFn); this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; this._resetLastSuggestion(); this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); } Dataset.extractData = function extractData(el) { var $el = $(el); if ($el.data(keys.obj)) { return { dataset: $el.data(keys.dataset) || "", val: $el.data(keys.val) || "", obj: $el.data(keys.obj) || null }; } return null; }; _.mixin(Dataset.prototype, EventEmitter, { _overwrite: function overwrite(query, suggestions) { suggestions = suggestions || []; if (suggestions.length) { this._renderSuggestions(query, suggestions); } else if (this.async && this.templates.pending) { this._renderPending(query); } else if (!this.async && this.templates.notFound) { this._renderNotFound(query); } else { this._empty(); } this.trigger("rendered", suggestions, false, this.name); }, _append: function append(query, suggestions) { suggestions = suggestions || []; if (suggestions.length && this.$lastSuggestion.length) { this._appendSuggestions(query, suggestions); } else if (suggestions.length) { this._renderSuggestions(query, suggestions); } else if (!this.$lastSuggestion.length && this.templates.notFound) { this._renderNotFound(query); } this.trigger("rendered", suggestions, true, this.name); }, _renderSuggestions: function renderSuggestions(query, suggestions) { var $fragment; $fragment = this._getSuggestionsFragment(query, suggestions); this.$lastSuggestion = $fragment.children().last(); this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); }, _appendSuggestions: function appendSuggestions(query, suggestions) { var $fragment, $lastSuggestion; $fragment = this._getSuggestionsFragment(query, suggestions); $lastSuggestion = $fragment.children().last(); this.$lastSuggestion.after($fragment); this.$lastSuggestion = $lastSuggestion; }, _renderPending: function renderPending(query) { var template = this.templates.pending; this._resetLastSuggestion(); template && this.$el.html(template({ query: query, dataset: this.name })); }, _renderNotFound: function renderNotFound(query) { var template = this.templates.notFound; this._resetLastSuggestion(); template && this.$el.html(template({ query: query, dataset: this.name })); }, _empty: function empty() { this.$el.empty(); this._resetLastSuggestion(); }, _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { var that = this, fragment; fragment = document.createDocumentFragment(); _.each(suggestions, function getSuggestionNode(suggestion) { var $el, context; context = that._injectQuery(query, suggestion); $el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); fragment.appendChild($el[0]); }); this.highlight && highlight({ className: this.classes.highlight, node: fragment, pattern: query }); return $(fragment); }, _getFooter: function getFooter(query, suggestions) { return this.templates.footer ? this.templates.footer({ query: query, suggestions: suggestions, dataset: this.name }) : null; }, _getHeader: function getHeader(query, suggestions) { return this.templates.header ? this.templates.header({ query: query, suggestions: suggestions, dataset: this.name }) : null; }, _resetLastSuggestion: function resetLastSuggestion() { this.$lastSuggestion = $(); }, _injectQuery: function injectQuery(query, obj) { return _.isObject(obj) ? _.mixin({ _query: query }, obj) : obj; }, update: function update(query) { var that = this, canceled = false, syncCalled = false, rendered = 0; this.cancel(); this.cancel = function cancel() { canceled = true; that.cancel = $.noop; that.async && that.trigger("asyncCanceled", query, that.name); }; this.source(query, sync, async); !syncCalled && sync([]); function sync(suggestions) { if (syncCalled) { return; } syncCalled = true; suggestions = (suggestions || []).slice(0, that.limit); rendered = suggestions.length; that._overwrite(query, suggestions); if (rendered < that.limit && that.async) { that.trigger("asyncRequested", query, that.name); } } function async(suggestions) { suggestions = suggestions || []; if (!canceled && rendered < that.limit) { that.cancel = $.noop; var idx = Math.abs(rendered - that.limit); rendered += idx; that._append(query, suggestions.slice(0, idx)); that.async && that.trigger("asyncReceived", query, that.name); } } }, cancel: $.noop, clear: function clear() { this._empty(); this.cancel(); this.trigger("cleared"); }, isEmpty: function isEmpty() { return this.$el.is(":empty"); }, destroy: function destroy() { this.$el = $("
"); } }); return Dataset; function getDisplayFn(display) { display = display || _.stringify; return _.isFunction(display) ? display : displayFn; function displayFn(obj) { return obj[display]; } } function getTemplates(templates, displayFn) { return { notFound: templates.notFound && _.templatify(templates.notFound), pending: templates.pending && _.templatify(templates.pending), header: templates.header && _.templatify(templates.header), footer: templates.footer && _.templatify(templates.footer), suggestion: templates.suggestion ? userSuggestionTemplate : suggestionTemplate }; function userSuggestionTemplate(context) { var template = templates.suggestion; return $(template(context)).attr("id", _.guid()); } function suggestionTemplate(context) { return $('
').attr("id", _.guid()).text(displayFn(context)); } } function isValidName(str) { return /^[_a-zA-Z0-9-]+$/.test(str); } }(); var Menu = function() { "use strict"; function Menu(o, www) { var that = this; o = o || {}; if (!o.node) { $.error("node is required"); } www.mixin(this); this.$node = $(o.node); this.query = null; this.datasets = _.map(o.datasets, initializeDataset); function initializeDataset(oDataset) { var node = that.$node.find(oDataset.node).first(); oDataset.node = node.length ? node : $("
").appendTo(that.$node); return new Dataset(oDataset, www); } } _.mixin(Menu.prototype, EventEmitter, { _onSelectableClick: function onSelectableClick($e) { this.trigger("selectableClicked", $($e.currentTarget)); }, _onRendered: function onRendered(type, dataset, suggestions, async) { this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); this.trigger("datasetRendered", dataset, suggestions, async); }, _onCleared: function onCleared() { this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); this.trigger("datasetCleared"); }, _propagate: function propagate() { this.trigger.apply(this, arguments); }, _allDatasetsEmpty: function allDatasetsEmpty() { return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) { var isEmpty = dataset.isEmpty(); this.$node.attr("aria-expanded", !isEmpty); return isEmpty; }, this)); }, _getSelectables: function getSelectables() { return this.$node.find(this.selectors.selectable); }, _removeCursor: function _removeCursor() { var $selectable = this.getActiveSelectable(); $selectable && $selectable.removeClass(this.classes.cursor); }, _ensureVisible: function ensureVisible($el) { var elTop, elBottom, nodeScrollTop, nodeHeight; elTop = $el.position().top; elBottom = elTop + $el.outerHeight(true); nodeScrollTop = this.$node.scrollTop(); nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); if (elTop < 0) { this.$node.scrollTop(nodeScrollTop + elTop); } else if (nodeHeight < elBottom) { this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); } }, bind: function() { var that = this, onSelectableClick; onSelectableClick = _.bind(this._onSelectableClick, this); this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); this.$node.on("mouseover", this.selectors.selectable, function() { that.setCursor($(this)); }); this.$node.on("mouseleave", function() { that._removeCursor(); }); _.each(this.datasets, function(dataset) { dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); }); return this; }, isOpen: function isOpen() { return this.$node.hasClass(this.classes.open); }, open: function open() { this.$node.scrollTop(0); this.$node.addClass(this.classes.open); }, close: function close() { this.$node.attr("aria-expanded", false); this.$node.removeClass(this.classes.open); this._removeCursor(); }, setLanguageDirection: function setLanguageDirection(dir) { this.$node.attr("dir", dir); }, selectableRelativeToCursor: function selectableRelativeToCursor(delta) { var $selectables, $oldCursor, oldIndex, newIndex; $oldCursor = this.getActiveSelectable(); $selectables = this._getSelectables(); oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; newIndex = oldIndex + delta; newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; return newIndex === -1 ? null : $selectables.eq(newIndex); }, setCursor: function setCursor($selectable) { this._removeCursor(); if ($selectable = $selectable && $selectable.first()) { $selectable.addClass(this.classes.cursor); this._ensureVisible($selectable); } }, getSelectableData: function getSelectableData($el) { return $el && $el.length ? Dataset.extractData($el) : null; }, getActiveSelectable: function getActiveSelectable() { var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); return $selectable.length ? $selectable : null; }, getTopSelectable: function getTopSelectable() { var $selectable = this._getSelectables().first(); return $selectable.length ? $selectable : null; }, update: function update(query) { var isValidUpdate = query !== this.query; if (isValidUpdate) { this.query = query; _.each(this.datasets, updateDataset); } return isValidUpdate; function updateDataset(dataset) { dataset.update(query); } }, empty: function empty() { _.each(this.datasets, clearDataset); this.query = null; this.$node.addClass(this.classes.empty); function clearDataset(dataset) { dataset.clear(); } }, destroy: function destroy() { this.$node.off(".tt"); this.$node = $("
"); _.each(this.datasets, destroyDataset); function destroyDataset(dataset) { dataset.destroy(); } } }); return Menu; }(); var Status = function() { "use strict"; function Status(options) { this.$el = $("", { role: "status", "aria-live": "polite" }).css({ position: "absolute", padding: "0", border: "0", height: "1px", width: "1px", "margin-bottom": "-1px", "margin-right": "-1px", overflow: "hidden", clip: "rect(0 0 0 0)", "white-space": "nowrap" }); options.$input.after(this.$el); _.each(options.menu.datasets, _.bind(function(dataset) { if (dataset.onSync) { dataset.onSync("rendered", _.bind(this.update, this)); dataset.onSync("cleared", _.bind(this.cleared, this)); } }, this)); } _.mixin(Status.prototype, { update: function update(event, suggestions) { var length = suggestions.length; var words; if (length === 1) { words = { result: "result", is: "is" }; } else { words = { result: "results", is: "are" }; } this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate."); }, cleared: function() { this.$el.text(""); } }); return Status; }(); var DefaultMenu = function() { "use strict"; var s = Menu.prototype; function DefaultMenu() { Menu.apply(this, [].slice.call(arguments, 0)); } _.mixin(DefaultMenu.prototype, Menu.prototype, { open: function open() { !this._allDatasetsEmpty() && this._show(); return s.open.apply(this, [].slice.call(arguments, 0)); }, close: function close() { this._hide(); return s.close.apply(this, [].slice.call(arguments, 0)); }, _onRendered: function onRendered() { if (this._allDatasetsEmpty()) { this._hide(); } else { this.isOpen() && this._show(); } return s._onRendered.apply(this, [].slice.call(arguments, 0)); }, _onCleared: function onCleared() { if (this._allDatasetsEmpty()) { this._hide(); } else { this.isOpen() && this._show(); } return s._onCleared.apply(this, [].slice.call(arguments, 0)); }, setLanguageDirection: function setLanguageDirection(dir) { this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); }, _hide: function hide() { this.$node.hide(); }, _show: function show() { this.$node.css("display", "block"); } }); return DefaultMenu; }(); var Typeahead = function() { "use strict"; function Typeahead(o, www) { var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; o = o || {}; if (!o.input) { $.error("missing input"); } if (!o.menu) { $.error("missing menu"); } if (!o.eventBus) { $.error("missing event bus"); } www.mixin(this); this.eventBus = o.eventBus; this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; this.input = o.input; this.menu = o.menu; this.enabled = true; this.autoselect = !!o.autoselect; this.active = false; this.input.hasFocus() && this.activate(); this.dir = this.input.getLangDir(); this._hacks(); this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); onFocused = c(this, "activate", "open", "_onFocused"); onBlurred = c(this, "deactivate", "_onBlurred"); onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); onEscKeyed = c(this, "isActive", "_onEscKeyed"); onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); } _.mixin(Typeahead.prototype, { _hacks: function hacks() { var $input, $menu; $input = this.input.$input || $("
"); $menu = this.menu.$node || $("
"); $input.on("blur.tt", function($e) { var active, isActive, hasActive; active = document.activeElement; isActive = $menu.is(active); hasActive = $menu.has(active).length > 0; if (_.isMsie() && (isActive || hasActive)) { $e.preventDefault(); $e.stopImmediatePropagation(); _.defer(function() { $input.focus(); }); } }); $menu.on("mousedown.tt", function($e) { $e.preventDefault(); }); }, _onSelectableClicked: function onSelectableClicked(type, $el) { this.select($el); }, _onDatasetCleared: function onDatasetCleared() { this._updateHint(); }, _onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) { this._updateHint(); if (this.autoselect) { var cursorClass = this.selectors.cursor.substr(1); this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass); } this.eventBus.trigger("render", suggestions, async, dataset); }, _onAsyncRequested: function onAsyncRequested(type, dataset, query) { this.eventBus.trigger("asyncrequest", query, dataset); }, _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { this.eventBus.trigger("asynccancel", query, dataset); }, _onAsyncReceived: function onAsyncReceived(type, dataset, query) { this.eventBus.trigger("asyncreceive", query, dataset); }, _onFocused: function onFocused() { this._minLengthMet() && this.menu.update(this.input.getQuery()); }, _onBlurred: function onBlurred() { if (this.input.hasQueryChangedSinceLastFocus()) { this.eventBus.trigger("change", this.input.getQuery()); } }, _onEnterKeyed: function onEnterKeyed(type, $e) { var $selectable; if ($selectable = this.menu.getActiveSelectable()) { if (this.select($selectable)) { $e.preventDefault(); $e.stopPropagation(); } } else if (this.autoselect) { if (this.select(this.menu.getTopSelectable())) { $e.preventDefault(); $e.stopPropagation(); } } }, _onTabKeyed: function onTabKeyed(type, $e) { var $selectable; if ($selectable = this.menu.getActiveSelectable()) { this.select($selectable) && $e.preventDefault(); } else if (this.autoselect) { if ($selectable = this.menu.getTopSelectable()) { this.autocomplete($selectable) && $e.preventDefault(); } } }, _onEscKeyed: function onEscKeyed() { this.close(); }, _onUpKeyed: function onUpKeyed() { this.moveCursor(-1); }, _onDownKeyed: function onDownKeyed() { this.moveCursor(+1); }, _onLeftKeyed: function onLeftKeyed() { if (this.dir === "rtl" && this.input.isCursorAtEnd()) { this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); } }, _onRightKeyed: function onRightKeyed() { if (this.dir === "ltr" && this.input.isCursorAtEnd()) { this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable()); } }, _onQueryChanged: function onQueryChanged(e, query) { this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); }, _onWhitespaceChanged: function onWhitespaceChanged() { this._updateHint(); }, _onLangDirChanged: function onLangDirChanged(e, dir) { if (this.dir !== dir) { this.dir = dir; this.menu.setLanguageDirection(dir); } }, _openIfActive: function openIfActive() { this.isActive() && this.open(); }, _minLengthMet: function minLengthMet(query) { query = _.isString(query) ? query : this.input.getQuery() || ""; return query.length >= this.minLength; }, _updateHint: function updateHint() { var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; $selectable = this.menu.getTopSelectable(); data = this.menu.getSelectableData($selectable); val = this.input.getInputValue(); if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { query = Input.normalizeQuery(val); escapedQuery = _.escapeRegExChars(query); frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); match = frontMatchRegEx.exec(data.val); match && this.input.setHint(val + match[1]); } else { this.input.clearHint(); } }, isEnabled: function isEnabled() { return this.enabled; }, enable: function enable() { this.enabled = true; }, disable: function disable() { this.enabled = false; }, isActive: function isActive() { return this.active; }, activate: function activate() { if (this.isActive()) { return true; } else if (!this.isEnabled() || this.eventBus.before("active")) { return false; } else { this.active = true; this.eventBus.trigger("active"); return true; } }, deactivate: function deactivate() { if (!this.isActive()) { return true; } else if (this.eventBus.before("idle")) { return false; } else { this.active = false; this.close(); this.eventBus.trigger("idle"); return true; } }, isOpen: function isOpen() { return this.menu.isOpen(); }, open: function open() { if (!this.isOpen() && !this.eventBus.before("open")) { this.input.setAriaExpanded(true); this.menu.open(); this._updateHint(); this.eventBus.trigger("open"); } return this.isOpen(); }, close: function close() { if (this.isOpen() && !this.eventBus.before("close")) { this.input.setAriaExpanded(false); this.menu.close(); this.input.clearHint(); this.input.resetInputValue(); this.eventBus.trigger("close"); } return !this.isOpen(); }, setVal: function setVal(val) { this.input.setQuery(_.toStr(val)); }, getVal: function getVal() { return this.input.getQuery(); }, select: function select($selectable) { var data = this.menu.getSelectableData($selectable); if (data && !this.eventBus.before("select", data.obj, data.dataset)) { this.input.setQuery(data.val, true); this.eventBus.trigger("select", data.obj, data.dataset); this.close(); return true; } return false; }, autocomplete: function autocomplete($selectable) { var query, data, isValid; query = this.input.getQuery(); data = this.menu.getSelectableData($selectable); isValid = data && query !== data.val; if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) { this.input.setQuery(data.val); this.eventBus.trigger("autocomplete", data.obj, data.dataset); return true; } return false; }, moveCursor: function moveCursor(delta) { var query, $candidate, data, suggestion, datasetName, cancelMove, id; query = this.input.getQuery(); $candidate = this.menu.selectableRelativeToCursor(delta); data = this.menu.getSelectableData($candidate); suggestion = data ? data.obj : null; datasetName = data ? data.dataset : null; id = $candidate ? $candidate.attr("id") : null; this.input.trigger("cursorchange", id); cancelMove = this._minLengthMet() && this.menu.update(query); if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) { this.menu.setCursor($candidate); if (data) { if (typeof data.val === "string") { this.input.setInputValue(data.val); } } else { this.input.resetInputValue(); this._updateHint(); } this.eventBus.trigger("cursorchange", suggestion, datasetName); return true; } return false; }, destroy: function destroy() { this.input.destroy(); this.menu.destroy(); } }); return Typeahead; function c(ctx) { var methods = [].slice.call(arguments, 1); return function() { var args = [].slice.call(arguments); _.each(methods, function(method) { return ctx[method].apply(ctx, args); }); }; } }(); (function() { "use strict"; var old, keys, methods; old = $.fn.typeahead; keys = { www: "tt-www", attrs: "tt-attrs", typeahead: "tt-typeahead" }; methods = { initialize: function initialize(o, datasets) { var www; datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); o = o || {}; www = WWW(o.classNames); return this.each(attach); function attach() { var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor; _.each(datasets, function(d) { d.highlight = !!o.highlight; }); $input = $(this); $wrapper = $(www.html.wrapper); $hint = $elOrNull(o.hint); $menu = $elOrNull(o.menu); defaultHint = o.hint !== false && !$hint; defaultMenu = o.menu !== false && !$menu; defaultHint && ($hint = buildHintFromInput($input, www)); defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); $hint && $hint.val(""); $input = prepInput($input, www); if (defaultHint || defaultMenu) { $wrapper.css(www.css.wrapper); $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); } MenuConstructor = defaultMenu ? DefaultMenu : Menu; eventBus = new EventBus({ el: $input }); input = new Input({ hint: $hint, input: $input, menu: $menu }, www); menu = new MenuConstructor({ node: $menu, datasets: datasets }, www); status = new Status({ $input: $input, menu: menu }); typeahead = new Typeahead({ input: input, menu: menu, eventBus: eventBus, minLength: o.minLength, autoselect: o.autoselect }, www); $input.data(keys.www, www); $input.data(keys.typeahead, typeahead); } }, isEnabled: function isEnabled() { var enabled; ttEach(this.first(), function(t) { enabled = t.isEnabled(); }); return enabled; }, enable: function enable() { ttEach(this, function(t) { t.enable(); }); return this; }, disable: function disable() { ttEach(this, function(t) { t.disable(); }); return this; }, isActive: function isActive() { var active; ttEach(this.first(), function(t) { active = t.isActive(); }); return active; }, activate: function activate() { ttEach(this, function(t) { t.activate(); }); return this; }, deactivate: function deactivate() { ttEach(this, function(t) { t.deactivate(); }); return this; }, isOpen: function isOpen() { var open; ttEach(this.first(), function(t) { open = t.isOpen(); }); return open; }, open: function open() { ttEach(this, function(t) { t.open(); }); return this; }, close: function close() { ttEach(this, function(t) { t.close(); }); return this; }, select: function select(el) { var success = false, $el = $(el); ttEach(this.first(), function(t) { success = t.select($el); }); return success; }, autocomplete: function autocomplete(el) { var success = false, $el = $(el); ttEach(this.first(), function(t) { success = t.autocomplete($el); }); return success; }, moveCursor: function moveCursoe(delta) { var success = false; ttEach(this.first(), function(t) { success = t.moveCursor(delta); }); return success; }, val: function val(newVal) { var query; if (!arguments.length) { ttEach(this.first(), function(t) { query = t.getVal(); }); return query; } else { ttEach(this, function(t) { t.setVal(_.toStr(newVal)); }); return this; } }, destroy: function destroy() { ttEach(this, function(typeahead, $input) { revert($input); typeahead.destroy(); }); return this; } }; $.fn.typeahead = function(method) { if (methods[method]) { return methods[method].apply(this, [].slice.call(arguments, 1)); } else { return methods.initialize.apply(this, arguments); } }; $.fn.typeahead.noConflict = function noConflict() { $.fn.typeahead = old; return this; }; function ttEach($els, fn) { $els.each(function() { var $input = $(this), typeahead; (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); }); } function buildHintFromInput($input, www) { return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({ readonly: true, required: false }).removeAttr("id name placeholder").removeClass("required").attr({ spellcheck: "false", tabindex: -1 }); } function prepInput($input, www) { $input.data(keys.attrs, { dir: $input.attr("dir"), autocomplete: $input.attr("autocomplete"), spellcheck: $input.attr("spellcheck"), style: $input.attr("style") }); $input.addClass(www.classes.input).attr({ spellcheck: false }); try { !$input.attr("dir") && $input.attr("dir", "auto"); } catch (e) {} return $input; } function getBackgroundStyles($el) { return { backgroundAttachment: $el.css("background-attachment"), backgroundClip: $el.css("background-clip"), backgroundColor: $el.css("background-color"), backgroundImage: $el.css("background-image"), backgroundOrigin: $el.css("background-origin"), backgroundPosition: $el.css("background-position"), backgroundRepeat: $el.css("background-repeat"), backgroundSize: $el.css("background-size") }; } function revert($input) { var www, $wrapper; www = $input.data(keys.www); $wrapper = $input.parent().filter(www.selectors.wrapper); _.each($input.data(keys.attrs), function(val, key) { _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); }); $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); if ($wrapper.length) { $input.detach().insertAfter($wrapper); $wrapper.remove(); } } function $elOrNull(obj) { var isValid, $el; isValid = _.isJQuery(obj) || _.isElement(obj); $el = isValid ? $(obj).first() : []; return $el.length ? $el : null; } })(); }); ================================================ FILE: docs/migration.html ================================================ Migration Reference

Migration

This guide provides details for how to migrate between major versions of IGListKit.

From 2.x to 3.x

For details on all changes in IGListKit 3.0.0, please see the release notes.

NOTE: This release contains a lot of improvements and source-breaking API changes, especially for Swift clients. These are all noted in the full release notes.

“IG” prefix removed for Swift

We have improved how IGListKit APIs get imported into Swift. The IG prefix has been removed for Swift clients. For example, IGListSectionController becomes ListSectionController instead. Along with other interoperability improvements, this makes IGListKit more readable in Swift.

To migrate, use Xcode’s Find navigator (command-3), search for IGList, and replace with List.

IGListSectionType removed

In order to make building section controllers even easier, we removed the protocol and absorbed all of the methods into IGListSectionController with default implementations.

  • numberOfItems returns 1 item
  • didUpdateToObject: and didSelectItemAtIndex: do nothing
  • sizeForItemAtIndex: returns CGSizeZero
  • cellForItemAtIndex: asserts (you must override this method)

In Objective-C, all you need to do is find & remove all uses of IGListSectionType. This includes IGListSectionController and IGListAdapterDataSource implementations.

In Swift, you will also need to add override keywords to all methods.

The compiler should catch all instances that need fixed.

IGListBindingSectionController

If you were using IGListDiff(...) inside a section controller to compute diffs for cells, we recommend that you start using IGListBindingSectionController which wraps this behavior in an elegant and tested API.

Removed IGListCollectionView

You can simply find regex IGListCollectionView([ |\*|\(]) and replace with regex UICollectionView$1 in your project to fix this.

Replace IGListCollectionView

Removed IGListGridCollectionViewLayout

Start using IGListCollectionViewLayout instead of IGListGridCollectionViewLayout.

  • scrollDirection is not yet supported. If you need horizontal scrolling, please use UICollectionViewFlowLayout or file an issue.
  • Set minimumLineSpacing on your section controllers instead of the layout
  • Set minimumInteritemSpacing on your section controllers instead of the layout
  • Return the size of your cells in sizeForItemAtIndex: instead of setting it on the layout.

Item mutations must be wrapped in -[IGListCollectionContext performBatchAnimated:completion:]

To fix some rare crashes, all item mutations must now be performed inside a batch block and done on the IGListBatchContext object instead.

Objective-C

// OLD
self.expanded = YES;
[self.collectionContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:]];

// NEW
[self.collectionContext performBatchAnimated:YES updates:^(id<IGListBatchContext> batchContext) {
  self.expanded = YES;
  [batchContext insertInSectionController:self atIndexes:[NSIndexSet indexSetWithIndex:1]];
} completion:nil];

Swift

// OLD
expanded = true
collectionContext?.insert(in: self, at: [0])

// NEW
collectionContext?.performBatch(animated: true, updates: { (batchContext) in
  self.expanded = true
  batchContext.insert(in: self, at: [0])
})

Make sure that your model changes occur inside the update block, alongside the context methods.

From 1.x to 2.x

For details on all changes in IGListKit 2.0.0, please see the release notes.

IGListDiffable Conformance

If you relied on the default NSObject<IGListDiffable> category, you will need to add IGListDiffable conformance to each of your models. To get things working as they did in 1.0, simply add the following to each of your models:

Objective-C

#import <IGListDiffKit/IGListDiffable.h>

// Header
@interface MyModel <IGListDiffable>

// Implementation
- (id<NSObject>)diffIdentifier {
  return self;
}

- (BOOL)isEqualToDiffableObject:(id<IGListDiffable>)object {
  return [self isEqual:object];
}

Swift

import IGListKit

extension MyModel: ListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return self
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    return isEqual(object)
  }
}

However we recommend writing more thorough identity and equality checks. Check out our guide to IGListDiffable and Equality for more info.

================================================ FILE: docs/modeling-and-binding.html ================================================ Modeling and Binding Reference

Modeling and Binding

This guide will walk you through a practical example of taking an app spec/design and turning it into a working IGListKit project.

You will learn how to:

  • Turn a design spec into a top-level model and view models
  • Use ListBindingSectionController for animated, one-way cell updates
  • Cell-to-controller action handling and delegation
  • Updating the UI with local data mutations

Getting Started

You can follow along and build the example in this guide. First, you must download this starter project. Open ModelingAndBinding-Starter/ModelingAndBinding.xcworkspace since the base project is setup with CocoaPods with IGListKit already added as a dependency.

Take a look at the following Instagram-inspired list element design:

Design Specs

You can already start mentally modelling your data:

  • The top cell has a username and timestamp label
  • The image cell will need some sort of image URL
  • An action cell with like count. There will also need to be some sort of action handling when someone taps the heart
  • Then there are a dynamic number of comment cells that contain a username and comment

Remember that IGListKit functions on one model per section controller. All of the cells in this design correlate to one top-level “post” object delivered by a server. You want to create a Post model that contains all of the information that the cells require.

A common mistake is to create a single model and section controller for a single cell. In this example, that will create a very confusing architecture since the top-level objects will contain a mix and match of user, image, action, and comment models.

Creating Models

Create a new file named Post.swift in the starter project:

import IGListKit

final class Post: ListDiffable {

  // 1
  let username: String
  let timestamp: String
  let imageURL: URL
  let likes: Int
  let comments: [Comment]

  // 2
  init(username: String, timestamp: String, imageURL: URL, likes: Int, comments: [Comment]) {
    self.username = username
    self.timestamp = timestamp
    self.imageURL = imageURL
    self.likes = likes
    self.comments = comments
  }

}
  1. It’s best practice to always declare your values as let so they cannot be mutated again. The compiler will complain about the Comment model, ignore that for now.
  2. Since

    Since IGListKit is compatible with Objective-C, your models must be classes which means writing initializers. It’s only a little copy & paste!

Now add a ListDiffable implementation inside of Post:

// MARK: ListDiffable

func diffIdentifier() -> NSObjectProtocol {
  // 1
  return (username + timestamp) as NSObjectProtocol
}

// 2
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
  return true
}
  1. Derive a unique identifier for each post. Since a single post should never have the same username and timestamp combo, we can start with that.
  2. A core requirement to using ListBindingSectionController is that if two models have the same diffIdentifier, they must be equal so that the section controller can then compare view models.

View Models

Create a new Swift file named Comment.swift and try writing the Comment model yourself:

  • username of type String
  • text of type String
  • You will be diffing this model eventually, so add a ListDiffable implementation

If you get stuck, or just want to copy & paste, you can reveal the implementation below.

Comment implementation

“`swift import IGListKit final class Comment: ListDiffable { let username: String let text: String init(username: String, text: String) { self.username = username self.text = text } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return (username + text) as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { return true } } ”` A note on the `isEqual(toDiffableObject:)` implementation: Whatever you use to derive the `diffIdentifier` can be omitted from any equality checks, since by definition the objects have already matched on their identifiers. In this case, the `username` and `text` **must be equal** by the time two objects are checked for equality.

Using the Comment array on a Post should make some sense: there are a dynamic number of comments on each post. For each comment, you want to display a cell.

What might be a little bit of a new concept, though, is that you need to create models for the UserCell, ImageCell, and ActionCell as well when working with ListBindingSectionController.

A binding section controller is almost like a mini-IGListKit. It takes an array of view models and turns them into configured cells. Get into the habit of creating a new model for each cell type within an ListBindingSectionController instance.

With that in mind, let’s start with the model for the UserCell:

Create a new Swift file called UserViewModel.swift:

import IGListKit

final class UserViewModel: ListDiffable {

  let username: String
  let timestamp: String

  init(username: String, timestamp: String) {
    self.username = username
    self.timestamp = timestamp
  }

  // MARK: ListDiffable

  func diffIdentifier() -> NSObjectProtocol {
    // 1
    return "user" as NSObjectProtocol
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    // 2
    guard let object = object as? UserViewModel else  { return false }
    return username == object.username
    && timestamp == object.timestamp
  }

}

    Since

    Since there will only be one UserViewModel per Post, you can hardcode an identifier. This will enforce only a single model & cell being used.
    • It’s important to write a good equality method for these view models. Anytime something changes, forcing the models to not be equal, the cell will be refreshed.

Try to make view models for the image and action cell. Remember there is only a single cell per Post, so you can use UserViewModel as a starting point for how the models should look.

View model implementations

“`swift import IGListKit final class ImageViewModel: ListDiffable { let url: URL init(url: URL) { self.url = url } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return "image” as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? ImageViewModel else { return false } return url == object.url } } final class ActionViewModel: ListDiffable { let likes: Int init(likes: Int) { self.likes = likes } // MARK: ListDiffable func diffIdentifier() -> NSObjectProtocol { return “action” as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? ActionViewModel else { return false } return likes == object.likes } } “` > You could try getting away with using generics since these models look so similar, but we’ve found that using **simple** models makes long-term maintenance more manageable.

Using ListBindingSectionController

You now have the following view models, which can all be derived from each Post:

  • UserViewModel
  • ImageViewModel
  • ActionViewModel
  • Comment

Let’s start using these models to power cells using ListBindingSectionController. This controller takes a top-level model (Post), asks its data source for an array of diffable view models (our view models above), then binds those view models to cells (provided in the starter project).

Binding Flow

Create PostSectionController.swift and add the following code:

final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource {

  override init() {
    super.init()
    dataSource = self
  }

}

Notice that you are subclassing ListBindingSectionController<Post>. This declares your section controller as receiving a Post model. That way you don’t have to do any special casting of your model.

There are 3 methods that are required to satisfy the data source protocol:

  • Return an array of view models given the top-level model (Post)
  • Return a size for a given view model
  • Return a cell for a given view model

First take care of the Post-to-view-models transformation:

// MARK: ListBindingSectionControllerDataSource

func sectionController(
  _ sectionController: ListBindingSectionController<ListDiffable>,
  viewModelsFor object: Any
  ) -> [ListDiffable] {
    // 1
    guard let object = object as? Post else { fatalError() }
    // 2
    let results: [ListDiffable] = [
      UserViewModel(username: object.username, timestamp: object.timestamp),
      ImageViewModel(url: object.imageURL),
      ActionViewModel(likes: object.likes)
    ]
    // 3
    return results + object.comments
}
  1. The object property must be optional because it will not exist upon section controller initialization. However, it should never be nil at this point, nor should the object: Any parameter be anything but the section controller type. This is a limitation of Objective-C generics and protocols, so doing a fatalError() here is appropriate.
  2. Create your array of view models by decomposing the Post model into smaller models.
  3. You can even append dynamic models that are delivered from the server.

Next add the required API to return a size for each view model:

func sectionController(
  _ sectionController: ListBindingSectionController<ListDiffable>,
  sizeForViewModel viewModel: Any,
  at index: Int
  ) -> CGSize {
  // 1
  guard let width = collectionContext?.containerSize.width else { fatalError() }
  // 2
  let height: CGFloat
  switch viewModel {
  case is ImageViewModel: height = 250
  case is Comment: height = 35
  // 3
  default: height = 55
  }
  return CGSize(width: width, height: height)
}
  1. Just like the object property, the collectionContext should never be nil, but it’s a weakly referenced object so must be declared as optional. Again, use fatalError() to catch any critical failures.
  2. Swift makes checking for types so easy! Just switch on the type and assign a height. In Objective-C you should use isKindOfClass:.
  3. Both the UserViewModel and ActionViewModel share the same height of 55pts according to the design.

Lastly, implement the API that returns a cell for each view model. This should look similar to the size API above. Give it a try yourself.

Remember that the cells are defined in Main.storyboard. You can click on each cell to view their identifiers.

"cellForViewModel:” implementation

“`swift func sectionController( _ sectionController: ListBindingSectionController, cellForViewModel viewModel: Any, at index: Int ) -> UICollectionViewCell { let identifier: String switch viewModel { case is ImageViewModel: identifier = "image” case is Comment: identifier = “comment” case is UserViewModel: identifier = “user” default: identifier = “action” } guard let cell = collectionContext? .dequeueReusableCellFromStoryboard(withIdentifier: identifier, for: self, at: index) else { fatalError() } return cell } “` Remember to handle `UserViewModel` and `ActionViewModel` separately!

Binding Models to Cells

Now you have PostSectionController setup to create view models, sizes, and cells. The last piece to using ListBindingSectionController is having your cells to receive its assigned view model and configure itself.

This is done by making your cells conform to ListBindable. With that, ListBindingSectionController will automatically bind view models to each cell!

Open ImageCell.swift and change the implementation to look like the following:

import UIKit
import SDWebImage
// 1
import IGListKit

// 2
final class ImageCell: UICollectionViewCell, ListBindable {

  @IBOutlet weak var imageView: UIImageView!

  // MARK: ListBindable

  func bindViewModel(_ viewModel: Any) {
    // 3
    guard let viewModel = viewModel as? ImageViewModel else { return }
    // 4
    imageView.sd_setImage(with: viewModel.url)
  }

}
  1. Make sure to import IGListKit!
  2. Have the cell conform to ListBindable
  3. Guard against the view model type. This will always be what PostSectionController pairs the cell with in cellForViewModel:, but guard to be safe.
  4. Use the SDWebImage library to set the image URL.

Now do exactly the same thing for each of the other cells:

  • ActionCell binds ActionViewModel
  • UserCell binds UserViewModel
  • CommentCell binds Comment
ListBindable implementations

”`swift final class ActionCell: UICollectionViewCell, ListBindable { @IBOutlet weak var likesLabel: UILabel! @IBOutlet weak var likeButton: UIButton! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? ActionViewModel else { return } likesLabel.text = “\(viewModel.likes)” } } final class UserCell: UICollectionViewCell, ListBindable { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? UserViewModel else { return } usernameLabel.text = viewModel.username dateLabel.text = viewModel.timestamp } } final class CommentCell: UICollectionViewCell, ListBindable { @IBOutlet weak var usernameLabel: UILabel! @IBOutlet weak var commentLabel: UILabel! // MARK: ListBindable func bindViewModel(_ viewModel: Any) { guard let viewModel = viewModel as? Comment else { return } usernameLabel.text = viewModel.username commentLabel.text = viewModel.text } } “`

Displaying in the View Controller

The very last step is getting the PostSectionController displaying in the app’s list.

Go back to ViewController.swift and add the following to viewDidLoad(), before setting the dataSource or collectionView:

data.append(Post(
  username: "@janedoe",
  timestamp: "15min",
  imageURL: URL(string: "https://placekitten.com/g/375/250")!,
  likes: 384,
  comments: [
    Comment(username: "@ryan", text: "this is beautiful!"),
    Comment(username: "@jsq", text: "😱"),
    Comment(username: "@caitlin", text: "#blessed"),
  ]
))

Lastly, update listAdapter(_, sectionControllerFor object:):

func listAdapter(
  _ listAdapter: ListAdapter,
  sectionControllerFor object: Any
  ) -> ListSectionController {
  return PostSectionController()
}

Normally you’d want to check the type of object, but since you’re only using Post at this point, it’s safe to simply return a new PostSectionController.

Build and run the sample app to see your post show up!

Working in the Simulator

Handling Cell Actions

This design should respond to tapping the heart icon on the ActionCell. In order to do that, you need to handle taps on the UIButton, then forward the event to the PostSectionController:

Open ActionCell.swift and add the following protocol:

protocol ActionCellDelegate: AnyObject {
  func didTapHeart(cell: ActionCell)
}

Add a new delegate variable to the ActionCell, beneath the outlets:

weak var delegate: ActionCellDelegate? = nil

Override awakeFromNib() and add a target & action to the likeButton:

override func awakeFromNib() {
  super.awakeFromNib()
  likeButton.addTarget(self, action: #selector(ActionCell.onHeart), for: .touchUpInside)
}

The last thing you need to do in ActionCell.swift is add an implementation for onHeart():

func onHeart() {
  delegate?.didTapHeart(cell: self)
}

This will forward the button tap outside of the cell and to the delegate.

Open PostSectionController.swift and update the cellForViewModel: method. Add the following at the end of the method, just after the guard and right before you return the cell:

if let cell = cell as? ActionCell {
  cell.delegate = self
}

The compiler will immediately complain. Satisfy the compiler but adding an empty implementation to PostSectionController:

final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource,
ActionCellDelegate {

//...

// MARK: ActionCellDelegate

func didTapHeart(cell: ActionCell) {
  print("like")
}

Build and run the app and tap on the heart button. You should see "like"s printing into the console.

Local Mutations

Every time someone taps the heart button, you need to add a new like to the Post. However, all of your models are declared with let because immutable models are a much safer design. But if everything is immutable, how do we mutate the like count?

The PostSectionController is the perfect place to handle and store mutations. Open PostSectionController.swift and add the following variable:

var localLikes: Int? = nil

Go back to the didTapHeart(cell:) delegate method and change the implementation to the following:

func didTapHeart(cell: ActionCell) {
  // 1
  localLikes = (localLikes ?? object?.likes ?? 0) + 1
  // 2
  update(animated: true)
}
  1. Mutate the localLikes variable using either the previous localLikes or starting with object.likes, whichever exists. Fallback to 0 which will never happen, just satisfying the compiler.
  2. Call the update(animated:,completion:) API on ListBindingSectionController to refresh the cells on the screen.

In order to actually send the mutations to the models, you need to start using localLikes with the ActionViewModel which is given to the ActionCell.

Still in PostSectionController.swift, find the cellForViewModel: API and change the ActionViewModel initialization to the following:

ActionViewModel(likes: localLikes ?? object.likes)

Build and run the app, tap on the heart button, and see your likes increment!

Wrapping up

If you got stuck at all, or just want to play around with the example, you can find the finished project here in ModelingAndBinding/ModelingAndBinding.xcworkspace.

ListBindingSectionController is one of the most powerful features that we’ve built for IGListKit because it further encourages you to design small, composable models, views, and controllers.

You can also use the section controller to handle any interactions, as well as deal with mutations, just like a controller should!

If you have suggestions for other topics you’d like to see, or want to offer a correction, please create a new issue!

================================================ FILE: docs/search.json ================================================ {"Structs/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isTracking":{"name":"isTracking","abstract":"\u003cp\u003ereturns YES if user has touched. may not yet have started dragging.\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Structs/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isDragging":{"name":"isDragging","abstract":"\u003cp\u003ereturns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Structs/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isDecelerating":{"name":"isDecelerating","abstract":"\u003cp\u003ereturns YES if user isn\u0026rsquo;t dragging (touch up) but scroll view is still moving.\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@enabled":{"name":"enabled","abstract":"\u003cp\u003eEnable adaptive coalescing, where we try to mininimize the update delay\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@minInterval":{"name":"minInterval","abstract":"\u003cp\u003eStart coalescing if the last update was within this interval\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@intervalIncrement":{"name":"intervalIncrement","abstract":"\u003cp\u003eIf we need to coalesce, increase the interval by this much for next time.\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@maxInterval":{"name":"maxInterval","abstract":"\u003cp\u003eThis is the maximum coalesce interval, so the slowest and update can wait.\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@useMaxIntervalWhenViewNotVisible":{"name":"useMaxIntervalWhenViewNotVisible","abstract":"\u003cp\u003eCoalece using \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbStructs/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@maxInterval\"\u003emaxInterval\u003c/a\u003e\u003c/code\u003e if view is not visible according to \u003ccode\u003eIGListViewVisibilityTracker\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Structs/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@enabled":{"name":"enabled","abstract":"\u003cp\u003eEnabled experimental code path. This needs to be enabled for the other properties to take effect.\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Structs/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@higherQOSEnabled":{"name":"higherQOSEnabled","abstract":"\u003cp\u003eEnable higher background thread priority\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Structs/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@maxItemCountToRunOnMain":{"name":"maxItemCountToRunOnMain","abstract":"\u003cp\u003eIf both item counts are under this number, we\u0026rsquo;ll run the diffing on the main thread.\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Structs/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@lowerPriorityWhenViewNotVisible":{"name":"lowerPriorityWhenViewNotVisible","abstract":"\u003cp\u003eLower QOS if view is not visible according to \u003ccode\u003eIGListViewVisibilityTracker\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Structs/IGListAdaptiveDiffingExperimentConfig.html":{"name":"IGListAdaptiveDiffingExperimentConfig","abstract":"\u003cp\u003eCustomize how diffing is performed\u003c/p\u003e"},"Structs/IGListAdaptiveCoalescingExperimentConfig.html":{"name":"IGListAdaptiveCoalescingExperimentConfig","abstract":"\u003cp\u003eCustomize how coalescing works to speed up some updates\u003c/p\u003e"},"Structs/IGListCollectionScrollingTraits.html":{"name":"IGListCollectionScrollingTraits","abstract":"\u003cp\u003eThe current scrolling traits of the underlying collection view."},"Functions.html#/c:@F@IGListDiff":{"name":"IGListDiff","abstract":"\u003cp\u003eCreates a diff using indexes between two collections.\u003c/p\u003e"},"Functions.html#/c:@F@IGListDiffPaths":{"name":"IGListDiffPaths","abstract":"\u003cp\u003eCreates a diff using index paths between two collections.\u003c/p\u003e"},"Functions.html#/c:IGListExperiments.h@F@IGListExperimentEnabled":{"name":"IGListExperimentEnabled","abstract":"\u003cp\u003eCheck if an experiment is enabled in a bitmask.\u003c/p\u003e"},"Type%20Definitions/IGListCollectionScrollingTraits/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isTracking":{"name":"isTracking","abstract":"\u003cp\u003ereturns YES if user has touched. may not yet have started dragging.\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Type%20Definitions/IGListCollectionScrollingTraits/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isDragging":{"name":"isDragging","abstract":"\u003cp\u003ereturns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Type%20Definitions/IGListCollectionScrollingTraits/IGListCollectionScrollingTraits.html#/c:@S@IGListCollectionScrollingTraits@FI@isDecelerating":{"name":"isDecelerating","abstract":"\u003cp\u003ereturns YES if user isn\u0026rsquo;t dragging (touch up) but scroll view is still moving.\u003c/p\u003e","parent_name":"IGListCollectionScrollingTraits"},"Type%20Definitions/IGListCollectionScrollingTraits/IGListCollectionScrollingTraits.html":{"name":"IGListCollectionScrollingTraits","abstract":"\u003cp\u003eThe current scrolling traits of the underlying collection view.","parent_name":"IGListCollectionScrollingTraits"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@enabled":{"name":"enabled","abstract":"\u003cp\u003eEnable adaptive coalescing, where we try to mininimize the update delay\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@minInterval":{"name":"minInterval","abstract":"\u003cp\u003eStart coalescing if the last update was within this interval\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@intervalIncrement":{"name":"intervalIncrement","abstract":"\u003cp\u003eIf we need to coalesce, increase the interval by this much for next time.\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@maxInterval":{"name":"maxInterval","abstract":"\u003cp\u003eThis is the maximum coalesce interval, so the slowest and update can wait.\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html#/c:@S@IGListAdaptiveCoalescingExperimentConfig@FI@useMaxIntervalWhenViewNotVisible":{"name":"useMaxIntervalWhenViewNotVisible","abstract":"\u003cp\u003eCoalece using \u003ccode\u003emaxInterval\u003c/code\u003e if view is not visible according to \u003ccode\u003eIGListViewVisibilityTracker\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig/IGListAdaptiveCoalescingExperimentConfig.html":{"name":"IGListAdaptiveCoalescingExperimentConfig","abstract":"\u003cp\u003eCustomize how coalescing works to speed up some updates\u003c/p\u003e","parent_name":"IGListAdaptiveCoalescingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@enabled":{"name":"enabled","abstract":"\u003cp\u003eEnabled experimental code path. This needs to be enabled for the other properties to take effect.\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@higherQOSEnabled":{"name":"higherQOSEnabled","abstract":"\u003cp\u003eEnable higher background thread priority\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@maxItemCountToRunOnMain":{"name":"maxItemCountToRunOnMain","abstract":"\u003cp\u003eIf both item counts are under this number, we\u0026rsquo;ll run the diffing on the main thread.\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html#/c:@S@IGListAdaptiveDiffingExperimentConfig@FI@lowerPriorityWhenViewNotVisible":{"name":"lowerPriorityWhenViewNotVisible","abstract":"\u003cp\u003eLower QOS if view is not visible according to \u003ccode\u003eIGListViewVisibilityTracker\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig/IGListAdaptiveDiffingExperimentConfig.html":{"name":"IGListAdaptiveDiffingExperimentConfig","abstract":"\u003cp\u003eCustomize how diffing is performed\u003c/p\u003e","parent_name":"IGListAdaptiveDiffingExperimentConfig"},"Type%20Definitions/IGListAdaptiveDiffingExperimentConfig.html":{"name":"IGListAdaptiveDiffingExperimentConfig","abstract":"\u003cp\u003eCustomize how diffing is performed\u003c/p\u003e"},"Type%20Definitions/IGListAdaptiveCoalescingExperimentConfig.html":{"name":"IGListAdaptiveCoalescingExperimentConfig","abstract":"\u003cp\u003eCustomize how coalescing works to speed up some updates\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListAdapter.h@T@IGListUpdaterCompletion":{"name":"IGListUpdaterCompletion","abstract":"\u003cp\u003eA block to execute when the list updates are completed.\u003c/p\u003e"},"Type%20Definitions/IGListCollectionScrollingTraits.html":{"name":"IGListCollectionScrollingTraits","abstract":"\u003cp\u003eThe current scrolling traits of the underlying collection view."},"Type%20Definitions.html#/c:IGListSingleSectionController.h@T@IGListSingleSectionCellConfigureBlock":{"name":"IGListSingleSectionCellConfigureBlock","abstract":"\u003cp\u003eA block used to configure cells.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListSingleSectionController.h@T@IGListSingleSectionCellSizeBlock":{"name":"IGListSingleSectionCellSizeBlock","abstract":"\u003cp\u003eA block that returns the size for the cell given the collection context.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListUpdatingCompletion":{"name":"IGListUpdatingCompletion","abstract":"\u003cp\u003eA completion block to execute when updates are finished.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListObjectTransitionBlock":{"name":"IGListObjectTransitionBlock","abstract":"\u003cp\u003eA block to be called when the adapter applies changes to the collection view.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListItemUpdateBlock":{"name":"IGListItemUpdateBlock","abstract":"\u003cp\u003eA block that contains all of the updates.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListReloadUpdateBlock":{"name":"IGListReloadUpdateBlock","abstract":"\u003cp\u003eA block to be called when an adapter reloads the collection view.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListToObjectBlock":{"name":"IGListToObjectBlock","abstract":"\u003cp\u003eA block that returns an array of objects to transition to.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListCollectionViewBlock":{"name":"IGListCollectionViewBlock","abstract":"\u003cp\u003eA block that returns a collection view to perform updates on.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListDataSourceChangeBlock":{"name":"IGListDataSourceChangeBlock","abstract":"\u003cp\u003eA block that applies a \u003ccode\u003eUICollectionView\u003c/code\u003e dataSource change\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListTransitionDataBlock":{"name":"IGListTransitionDataBlock","abstract":"\u003cp\u003eA block that returns the \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListTransitionData.html\"\u003eIGListTransitionData\u003c/a\u003e\u003c/code\u003e needed before an update.\u003c/p\u003e"},"Type%20Definitions.html#/c:IGListUpdatingDelegate.h@T@IGListTransitionDataApplyBlock":{"name":"IGListTransitionDataApplyBlock","abstract":"\u003cp\u003eA block to be called when the adapter applies changes to the collection view.\u003c/p\u003e"},"Protocols/IGListWorkingRangeDelegate.html#/c:objc(pl)IGListWorkingRangeDelegate(im)listAdapter:sectionControllerWillEnterWorkingRange:":{"name":"-listAdapter:sectionControllerWillEnterWorkingRange:","abstract":"\u003cp\u003eNotifies the delegate that an section controller will enter the working range.\u003c/p\u003e","parent_name":"IGListWorkingRangeDelegate"},"Protocols/IGListWorkingRangeDelegate.html#/c:objc(pl)IGListWorkingRangeDelegate(im)listAdapter:sectionControllerDidExitWorkingRange:":{"name":"-listAdapter:sectionControllerDidExitWorkingRange:","abstract":"\u003cp\u003eNotifies the delegate that an section controller exited the working range.\u003c/p\u003e","parent_name":"IGListWorkingRangeDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)objectLookupPointerFunctions":{"name":"-objectLookupPointerFunctions","abstract":"\u003cp\u003eAsks the delegate for the pointer functions for looking up an object in a collection.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)performUpdateWithCollectionViewBlock:animated:sectionDataBlock:applySectionDataBlock:completion:":{"name":"-performUpdateWithCollectionViewBlock:animated:sectionDataBlock:applySectionDataBlock:completion:","abstract":"\u003cp\u003ePerform a \u003cstrong\u003esection\u003c/strong\u003e update from an old array of objects to a new one.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)performUpdateWithCollectionViewBlock:animated:itemUpdates:completion:":{"name":"-performUpdateWithCollectionViewBlock:animated:itemUpdates:completion:","abstract":"\u003cp\u003ePerform an \u003cstrong\u003eitem\u003c/strong\u003e update block in the collection view.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)performDataSourceChange:":{"name":"-performDataSourceChange:","abstract":"\u003cp\u003ePerform a \u003ccode\u003e[UICollectionView setDataSource:...]\u003c/code\u003e swap within this block. It gives the updater the chance to cancel or","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)reloadDataWithCollectionViewBlock:reloadUpdateBlock:completion:":{"name":"-reloadDataWithCollectionViewBlock:reloadUpdateBlock:completion:","abstract":"\u003cp\u003eCompletely reload data in the collection.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)insertItemsIntoCollectionView:indexPaths:":{"name":"-insertItemsIntoCollectionView:indexPaths:","abstract":"\u003cp\u003eTells the delegate to perform item inserts at the given index paths.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)deleteItemsFromCollectionView:indexPaths:":{"name":"-deleteItemsFromCollectionView:indexPaths:","abstract":"\u003cp\u003eTells the delegate to perform item deletes at the given index paths.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)moveItemInCollectionView:fromIndexPath:toIndexPath:":{"name":"-moveItemInCollectionView:fromIndexPath:toIndexPath:","abstract":"\u003cp\u003eTells the delegate to move an item from and to given index paths.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)reloadItemInCollectionView:fromIndexPath:toIndexPath:":{"name":"-reloadItemInCollectionView:fromIndexPath:toIndexPath:","abstract":"\u003cp\u003eTells the delegate to reload an item from and to given index paths.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)moveSectionInCollectionView:fromIndex:toIndex:":{"name":"-moveSectionInCollectionView:fromIndex:toIndex:","abstract":"\u003cp\u003eTells the delegate to move a section from and to given indexes.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)reloadCollectionView:sections:":{"name":"-reloadCollectionView:sections:","abstract":"\u003cp\u003eCompletely reload each section in the collection view.\u003c/p\u003e","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)isInDataUpdateBlock":{"name":"-isInDataUpdateBlock","abstract":"\u003cp\u003eTrue if the updater is currently updating the source of truth, like executing applySectionDataBlock and","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListUpdatingDelegate.html#/c:objc(pl)IGListUpdatingDelegate(im)willCrashWithCollectionView:sectionControllerClass:":{"name":"-willCrashWithCollectionView:sectionControllerClass:","abstract":"\u003cp\u003eCalled when the updater detects an imminent crash, such as when a section controller returns a nil cell.","parent_name":"IGListUpdatingDelegate"},"Protocols/IGListTransitionDelegate.html#/c:objc(pl)IGListTransitionDelegate(im)listAdapter:customizedInitialLayoutAttributes:sectionController:atIndex:":{"name":"-listAdapter:customizedInitialLayoutAttributes:sectionController:atIndex:","abstract":"\u003cp\u003eAsks the delegate to customize and return the starting layout information for an item being inserted into the collection view.\u003c/p\u003e","parent_name":"IGListTransitionDelegate"},"Protocols/IGListTransitionDelegate.html#/c:objc(pl)IGListTransitionDelegate(im)listAdapter:customizedFinalLayoutAttributes:sectionController:atIndex:":{"name":"-listAdapter:customizedFinalLayoutAttributes:sectionController:atIndex:","abstract":"\u003cp\u003eAsks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view.\u003c/p\u003e","parent_name":"IGListTransitionDelegate"},"Protocols/IGListSupplementaryViewSource.html#/c:objc(pl)IGListSupplementaryViewSource(im)supportedElementKinds":{"name":"-supportedElementKinds","abstract":"\u003cp\u003eAsks the SupplementaryViewSource for an array of supported element kinds.\u003c/p\u003e","parent_name":"IGListSupplementaryViewSource"},"Protocols/IGListSupplementaryViewSource.html#/c:objc(pl)IGListSupplementaryViewSource(im)viewForSupplementaryElementOfKind:atIndex:":{"name":"-viewForSupplementaryElementOfKind:atIndex:","abstract":"\u003cp\u003eAsks the SupplementaryViewSource for a configured supplementary view for the specified kind and index.\u003c/p\u003e","parent_name":"IGListSupplementaryViewSource"},"Protocols/IGListSupplementaryViewSource.html#/c:objc(pl)IGListSupplementaryViewSource(im)sizeForSupplementaryViewOfKind:atIndex:":{"name":"-sizeForSupplementaryViewOfKind:atIndex:","abstract":"\u003cp\u003eAsks the SupplementaryViewSource for the size of a supplementary view for the given kind and index path.\u003c/p\u003e","parent_name":"IGListSupplementaryViewSource"},"Protocols/IGListSingleSectionControllerDelegate.html#/c:objc(pl)IGListSingleSectionControllerDelegate(im)didSelectSectionController:withObject:":{"name":"-didSelectSectionController:withObject:","abstract":"\u003cp\u003eTells the delegate that the section controller was selected.\u003c/p\u003e","parent_name":"IGListSingleSectionControllerDelegate"},"Protocols/IGListSingleSectionControllerDelegate.html#/c:objc(pl)IGListSingleSectionControllerDelegate(im)didDeselectSectionController:withObject:":{"name":"-didDeselectSectionController:withObject:","abstract":"\u003cp\u003eTells the delegate that the section controller was deselected.\u003c/p\u003e","parent_name":"IGListSingleSectionControllerDelegate"},"Protocols/IGListScrollDelegate.html#/c:objc(pl)IGListScrollDelegate(im)listAdapter:didScrollSectionController:":{"name":"-listAdapter:didScrollSectionController:","abstract":"\u003cp\u003eTells the delegate that the section controller was scrolled on screen.\u003c/p\u003e","parent_name":"IGListScrollDelegate"},"Protocols/IGListScrollDelegate.html#/c:objc(pl)IGListScrollDelegate(im)listAdapter:willBeginDraggingSectionController:":{"name":"-listAdapter:willBeginDraggingSectionController:","abstract":"\u003cp\u003eTells the delegate that the section controller will be dragged on screen.\u003c/p\u003e","parent_name":"IGListScrollDelegate"},"Protocols/IGListScrollDelegate.html#/c:objc(pl)IGListScrollDelegate(im)listAdapter:didEndDraggingSectionController:willDecelerate:":{"name":"-listAdapter:didEndDraggingSectionController:willDecelerate:","abstract":"\u003cp\u003eTells the delegate that the section controller did end dragging on screen.\u003c/p\u003e","parent_name":"IGListScrollDelegate"},"Protocols/IGListScrollDelegate.html#/c:objc(pl)IGListScrollDelegate(im)listAdapter:didEndDeceleratingSectionController:":{"name":"-listAdapter:didEndDeceleratingSectionController:","abstract":"\u003cp\u003eTells the delegate that the section controller did end decelerating on screen.\u003c/p\u003e","parent_name":"IGListScrollDelegate"},"Protocols/IGListDisplayDelegate.html#/c:objc(pl)IGListDisplayDelegate(im)listAdapter:willDisplaySectionController:":{"name":"-listAdapter:willDisplaySectionController:","abstract":"\u003cp\u003eTells the delegate that the specified section controller is about to be displayed.\u003c/p\u003e","parent_name":"IGListDisplayDelegate"},"Protocols/IGListDisplayDelegate.html#/c:objc(pl)IGListDisplayDelegate(im)listAdapter:didEndDisplayingSectionController:":{"name":"-listAdapter:didEndDisplayingSectionController:","abstract":"\u003cp\u003eTells the delegate that the specified section controller is no longer being displayed.\u003c/p\u003e","parent_name":"IGListDisplayDelegate"},"Protocols/IGListDisplayDelegate.html#/c:objc(pl)IGListDisplayDelegate(im)listAdapter:willDisplaySectionController:cell:atIndex:":{"name":"-listAdapter:willDisplaySectionController:cell:atIndex:","abstract":"\u003cp\u003eTells the delegate that a cell in the specified list is about to be displayed.\u003c/p\u003e","parent_name":"IGListDisplayDelegate"},"Protocols/IGListDisplayDelegate.html#/c:objc(pl)IGListDisplayDelegate(im)listAdapter:didEndDisplayingSectionController:cell:atIndex:":{"name":"-listAdapter:didEndDisplayingSectionController:cell:atIndex:","abstract":"\u003cp\u003eTells the delegate that a cell in the specified list is no longer being displayed.\u003c/p\u003e","parent_name":"IGListDisplayDelegate"},"Protocols/IGListCollectionViewLayoutCompatible.html#/c:objc(pl)IGListCollectionViewLayoutCompatible(im)didModifySection:":{"name":"-didModifySection:","abstract":"\u003cp\u003eCalled to notify the layout that a specific section was modified before invalidation. This can be used to optimize","parent_name":"IGListCollectionViewLayoutCompatible"},"Protocols/IGListCollectionViewDelegateLayout.html#/c:objc(pl)IGListCollectionViewDelegateLayout(im)collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:":{"name":"-collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:","abstract":"\u003cp\u003eAsks the delegate to customize and return the starting layout information for an item being inserted into the collection view.\u003c/p\u003e","parent_name":"IGListCollectionViewDelegateLayout"},"Protocols/IGListCollectionViewDelegateLayout.html#/c:objc(pl)IGListCollectionViewDelegateLayout(im)collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:":{"name":"-collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:","abstract":"\u003cp\u003eAsks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view.\u003c/p\u003e","parent_name":"IGListCollectionViewDelegateLayout"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)containerSize":{"name":"containerSize","abstract":"\u003cp\u003eThe size of the collection view. You can use this for sizing cells.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)containerInset":{"name":"containerInset","abstract":"\u003cp\u003eThe content insets of the collection view. You can use this for sizing cells.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)adjustedContainerInset":{"name":"adjustedContainerInset","abstract":"\u003cp\u003eThe adjusted content insets of the collection view. Equivalent to containerInset under iOS 11.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)insetContainerSize":{"name":"insetContainerSize","abstract":"\u003cp\u003eThe size of the collection view with content insets applied.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)containerContentOffset":{"name":"containerContentOffset","abstract":"\u003cp\u003eThe content offset of the collection view.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)traitCollection":{"name":"traitCollection","abstract":"\u003cp\u003eThe trait collection of the collection view.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)scrollingTraits":{"name":"scrollingTraits","abstract":"\u003cp\u003eThe current scrolling traits of the underlying collection view.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(py)experiments":{"name":"experiments","abstract":"\u003cp\u003eA bitmask of experiments to conduct on the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)containerSizeForSectionController:":{"name":"-containerSizeForSectionController:","abstract":"\u003cp\u003eReturns size of the collection view relative to the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)indexForCell:sectionController:":{"name":"-indexForCell:sectionController:","abstract":"\u003cp\u003eReturns the index of the specified cell in the collection relative to the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)cellForItemAtIndex:sectionController:":{"name":"-cellForItemAtIndex:sectionController:","abstract":"\u003cp\u003eReturns the cell in the collection at the specified index for the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)viewForSupplementaryElementOfKind:atIndex:sectionController:":{"name":"-viewForSupplementaryElementOfKind:atIndex:sectionController:","abstract":"\u003cp\u003eReturns the supplementary view in the collection at the specified index for the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)fullyVisibleCellsForSectionController:":{"name":"-fullyVisibleCellsForSectionController:","abstract":"\u003cp\u003eReturns the fully visible cells for the given section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)visibleCellsForSectionController:":{"name":"-visibleCellsForSectionController:","abstract":"\u003cp\u003eReturns the visible cells for the given section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)visibleIndexPathsForSectionController:":{"name":"-visibleIndexPathsForSectionController:","abstract":"\u003cp\u003eReturns the visible paths for the given section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)deselectItemAtIndex:sectionController:animated:":{"name":"-deselectItemAtIndex:sectionController:animated:","abstract":"\u003cp\u003eDeselects a cell in the collection.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)selectItemAtIndex:sectionController:animated:scrollPosition:":{"name":"-selectItemAtIndex:sectionController:animated:scrollPosition:","abstract":"\u003cp\u003eSelects a cell in the collection.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableCellOfClass:withReuseIdentifier:forSectionController:atIndex:":{"name":"-dequeueReusableCellOfClass:withReuseIdentifier:forSectionController:atIndex:","abstract":"\u003cp\u003eDequeues a cell from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableCellOfClass:forSectionController:atIndex:":{"name":"-dequeueReusableCellOfClass:forSectionController:atIndex:","abstract":"\u003cp\u003eDequeues a cell from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableCellWithNibName:bundle:forSectionController:atIndex:":{"name":"-dequeueReusableCellWithNibName:bundle:forSectionController:atIndex:","abstract":"\u003cp\u003eDequeues a cell from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableCellFromStoryboardWithIdentifier:forSectionController:atIndex:":{"name":"-dequeueReusableCellFromStoryboardWithIdentifier:forSectionController:atIndex:","abstract":"\u003cp\u003eDequeues a storyboard prototype cell from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableSupplementaryViewOfKind:forSectionController:class:atIndex:":{"name":"-dequeueReusableSupplementaryViewOfKind:forSectionController:class:atIndex:","abstract":"\u003cp\u003eDequeues a supplementary view from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableSupplementaryViewFromStoryboardOfKind:withIdentifier:forSectionController:atIndex:":{"name":"-dequeueReusableSupplementaryViewFromStoryboardOfKind:withIdentifier:forSectionController:atIndex:","abstract":"\u003cp\u003eDequeues a supplementary view from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)dequeueReusableSupplementaryViewOfKind:forSectionController:nibName:bundle:atIndex:":{"name":"-dequeueReusableSupplementaryViewOfKind:forSectionController:nibName:bundle:atIndex:","abstract":"\u003cp\u003eDequeues a supplementary view from the collection view reuse pool.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)invalidateLayoutForSectionController:completion:":{"name":"-invalidateLayoutForSectionController:completion:","abstract":"\u003cp\u003eInvalidate the backing \u003ccode\u003eUICollectionViewLayout\u003c/code\u003e for all items in the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)layoutAttributesForItemAtIndex:sectionController:":{"name":"-layoutAttributesForItemAtIndex:sectionController:","abstract":"\u003cp\u003eReturns the layout attributes for the item at the specified index in the section controller.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)performBatchAnimated:updates:completion:":{"name":"-performBatchAnimated:updates:completion:","abstract":"\u003cp\u003eBatches and performs many cell-level updates in a single transaction.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)scrollToSectionController:atIndex:scrollPosition:animated:":{"name":"-scrollToSectionController:atIndex:scrollPosition:animated:","abstract":"\u003cp\u003eScrolls to the specified section controller in the list.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)indexPathForItemAtPoint:":{"name":"-indexPathForItemAtPoint:","abstract":"\u003cp\u003eReturns the index path of the item at the specified point in the collection view.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)convertPoint:fromView:":{"name":"-convertPoint:fromView:","abstract":"\u003cp\u003eConverts a point from the coordinate system of a given view to that of the collection view.\u003c/p\u003e","parent_name":"IGListCollectionContext"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html#/c:objc(pl)IGListBindingSectionControllerSelectionDelegate(im)sectionController:didSelectItemAtIndex:viewModel:":{"name":"-sectionController:didSelectItemAtIndex:viewModel:","abstract":"\u003cp\u003eTells the delegate that a cell at a given index was selected.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerSelectionDelegate"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html#/c:objc(pl)IGListBindingSectionControllerSelectionDelegate(im)sectionController:didDeselectItemAtIndex:viewModel:":{"name":"-sectionController:didDeselectItemAtIndex:viewModel:","abstract":"\u003cp\u003eTells the delegate that a cell at a given index was deselected.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerSelectionDelegate"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html#/c:objc(pl)IGListBindingSectionControllerSelectionDelegate(im)sectionController:didHighlightItemAtIndex:viewModel:":{"name":"-sectionController:didHighlightItemAtIndex:viewModel:","abstract":"\u003cp\u003eTells the delegate that a cell at a given index was highlighted.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerSelectionDelegate"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html#/c:objc(pl)IGListBindingSectionControllerSelectionDelegate(im)sectionController:didUnhighlightItemAtIndex:viewModel:":{"name":"-sectionController:didUnhighlightItemAtIndex:viewModel:","abstract":"\u003cp\u003eTells the delegate that a cell at a given index was unhighlighted.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerSelectionDelegate"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html#/c:objc(pl)IGListBindingSectionControllerSelectionDelegate(im)sectionController:contextMenuConfigurationForItemAtIndex:point:viewModel:":{"name":"-sectionController:contextMenuConfigurationForItemAtIndex:point:viewModel:","abstract":"\u003cp\u003eTells the delegate that a cell has requested a menu configuration.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerSelectionDelegate"},"Protocols/IGListBindingSectionControllerDataSource.html#/c:objc(pl)IGListBindingSectionControllerDataSource(im)sectionController:viewModelsForObject:":{"name":"-sectionController:viewModelsForObject:","abstract":"\u003cp\u003eCreate an array of view models given a top-level object.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerDataSource"},"Protocols/IGListBindingSectionControllerDataSource.html#/c:objc(pl)IGListBindingSectionControllerDataSource(im)sectionController:cellForViewModel:atIndex:":{"name":"-sectionController:cellForViewModel:atIndex:","abstract":"\u003cp\u003eReturn a dequeued cell for a given view model.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerDataSource"},"Protocols/IGListBindingSectionControllerDataSource.html#/c:objc(pl)IGListBindingSectionControllerDataSource(im)sectionController:sizeForViewModel:atIndex:":{"name":"-sectionController:sizeForViewModel:atIndex:","abstract":"\u003cp\u003eReturn a cell size for a given view model.\u003c/p\u003e","parent_name":"IGListBindingSectionControllerDataSource"},"Protocols/IGListBindable.html#/c:objc(pl)IGListBindable(im)bindViewModel:":{"name":"-bindViewModel:","abstract":"\u003cp\u003eTells the cell to configure itself with the given view model.\u003c/p\u003e","parent_name":"IGListBindable"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)reloadInSectionController:atIndexes:":{"name":"-reloadInSectionController:atIndexes:","abstract":"\u003cp\u003eReloads cells in the section controller.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)insertInSectionController:atIndexes:":{"name":"-insertInSectionController:atIndexes:","abstract":"\u003cp\u003eInserts cells in the list.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)deleteInSectionController:atIndexes:":{"name":"-deleteInSectionController:atIndexes:","abstract":"\u003cp\u003eDeletes cells in the list.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)invalidateLayoutInSectionController:atIndexes:":{"name":"-invalidateLayoutInSectionController:atIndexes:","abstract":"\u003cp\u003eInvalidates layouts of cells at specific in the section controller.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)moveInSectionController:fromIndex:toIndex:":{"name":"-moveInSectionController:fromIndex:toIndex:","abstract":"\u003cp\u003eMoves a cell from one index to another within the section controller.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)reloadSectionController:":{"name":"-reloadSectionController:","abstract":"\u003cp\u003eReloads the entire section controller.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)moveSectionControllerInteractive:fromIndex:toIndex:":{"name":"-moveSectionControllerInteractive:fromIndex:toIndex:","abstract":"\u003cp\u003eMoves a section controller from one index to another during interactive reordering.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)moveInSectionControllerInteractive:fromIndex:toIndex:":{"name":"-moveInSectionControllerInteractive:fromIndex:toIndex:","abstract":"\u003cp\u003eMoves an object within a section controller from one index to another during interactive reordering.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListBatchContext.html#/c:objc(pl)IGListBatchContext(im)revertInvalidInteractiveMoveFromIndexPath:toIndexPath:":{"name":"-revertInvalidInteractiveMoveFromIndexPath:toIndexPath:","abstract":"\u003cp\u003eReverts an move from one indexPath to another during interactive reordering.\u003c/p\u003e","parent_name":"IGListBatchContext"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willDiffFromObjects:toObjects:":{"name":"-listAdapterUpdater:willDiffFromObjects:toObjects:","abstract":"\u003cp\u003eNotifies the delegate that the updater is about to beging diffing.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:didDiffWithResults:onBackgroundThread:":{"name":"-listAdapterUpdater:didDiffWithResults:onBackgroundThread:","abstract":"\u003cp\u003eNotifies the delegate that the updater finished diffing.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willPerformBatchUpdatesWithCollectionView:fromObjects:toObjects:listIndexSetResult:animated:":{"name":"-listAdapterUpdater:willPerformBatchUpdatesWithCollectionView:fromObjects:toObjects:listIndexSetResult:animated:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView performBatchUpdates:completion:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:didPerformBatchUpdates:collectionView:":{"name":"-listAdapterUpdater:didPerformBatchUpdates:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater successfully finished \u003ccode\u003e-[UICollectionView performBatchUpdates:completion:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willInsertIndexPaths:collectionView:":{"name":"-listAdapterUpdater:willInsertIndexPaths:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView insertItemsAtIndexPaths:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willDeleteIndexPaths:collectionView:":{"name":"-listAdapterUpdater:willDeleteIndexPaths:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView deleteItemsAtIndexPaths:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willMoveFromIndexPath:toIndexPath:collectionView:":{"name":"-listAdapterUpdater:willMoveFromIndexPath:toIndexPath:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView moveItemAtIndexPath:toIndexPath:]\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willReloadIndexPaths:collectionView:":{"name":"-listAdapterUpdater:willReloadIndexPaths:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView reloadItemsAtIndexPaths:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willReloadSections:collectionView:":{"name":"-listAdapterUpdater:willReloadSections:collectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView reloadSections:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willReloadDataWithCollectionView:isFallbackReload:":{"name":"-listAdapterUpdater:willReloadDataWithCollectionView:isFallbackReload:","abstract":"\u003cp\u003eNotifies the delegate that the updater will call \u003ccode\u003e-[UICollectionView reloadData]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:didReloadDataWithCollectionView:isFallbackReload:":{"name":"-listAdapterUpdater:didReloadDataWithCollectionView:isFallbackReload:","abstract":"\u003cp\u003eNotifies the delegate that the updater successfully called \u003ccode\u003e-[UICollectionView reloadData]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:collectionView:willCrashWithException:fromObjects:toObjects:diffResult:updates:":{"name":"-listAdapterUpdater:collectionView:willCrashWithException:fromObjects:toObjects:diffResult:updates:","abstract":"\u003cp\u003eNotifies the delegate that the collection view threw an exception in \u003ccode\u003e-[UICollectionView performBatchUpdates:completion:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:willCrashWithCollectionView:sectionControllerClass:":{"name":"-listAdapterUpdater:willCrashWithCollectionView:sectionControllerClass:","abstract":"\u003cp\u003eNotifies the delegate that the updater detected an imminent crash, such as when a section controller returns a nil cell.","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdaterDelegate.html#/c:objc(pl)IGListAdapterUpdaterDelegate(im)listAdapterUpdater:didFinishWithoutUpdatesWithCollectionView:":{"name":"-listAdapterUpdater:didFinishWithoutUpdatesWithCollectionView:","abstract":"\u003cp\u003eNotifies the delegate that the updater finished without performing any batch updates or reloads\u003c/p\u003e","parent_name":"IGListAdapterUpdaterDelegate"},"Protocols/IGListAdapterUpdateListener.html#/c:objc(pl)IGListAdapterUpdateListener(im)listAdapter:didFinishUpdate:animated:":{"name":"-listAdapter:didFinishUpdate:animated:","abstract":"\u003cp\u003eNotifies a listener that the listAdapter was updated.\u003c/p\u003e","parent_name":"IGListAdapterUpdateListener"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapterWillCallDequeueCell:":{"name":"-listAdapterWillCallDequeueCell:","abstract":"\u003cp\u003eWill call \u003ccode\u003e-[IGListAdapter collectionView:cellForItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapter:didCallDequeueCell:onSectionController:atIndex:":{"name":"-listAdapter:didCallDequeueCell:onSectionController:atIndex:","abstract":"\u003cp\u003eDid finish calling \u003ccode\u003e-[IGListAdapter collectionView:cellForItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapterWillCallDisplayCell:":{"name":"-listAdapterWillCallDisplayCell:","abstract":"\u003cp\u003eWill call \u003ccode\u003e-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapter:didCallDisplayCell:onSectionController:atIndex:":{"name":"-listAdapter:didCallDisplayCell:onSectionController:atIndex:","abstract":"\u003cp\u003eDid finish calling \u003ccode\u003e-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapterWillCallEndDisplayCell:":{"name":"-listAdapterWillCallEndDisplayCell:","abstract":"\u003cp\u003eWill call \u003ccode\u003e-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapter:didCallEndDisplayCell:onSectionController:atIndex:":{"name":"-listAdapter:didCallEndDisplayCell:onSectionController:atIndex:","abstract":"\u003cp\u003eDid finish calling \u003ccode\u003e-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapterWillCallSize:":{"name":"-listAdapterWillCallSize:","abstract":"\u003cp\u003eWill call \u003ccode\u003e-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapter:didCallSizeOnSectionController:atIndex:":{"name":"-listAdapter:didCallSizeOnSectionController:atIndex:","abstract":"\u003cp\u003eDid finish calling \u003ccode\u003e-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapterWillCallScroll:":{"name":"-listAdapterWillCallScroll:","abstract":"\u003cp\u003eWill call \u003ccode\u003e-[IGListAdapter scrollViewDidScroll:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterPerformanceDelegate.html#/c:objc(pl)IGListAdapterPerformanceDelegate(im)listAdapter:didCallScroll:":{"name":"-listAdapter:didCallScroll:","abstract":"\u003cp\u003eDid finish calling \u003ccode\u003e-[IGListAdapter scrollViewDidScroll:]\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterPerformanceDelegate"},"Protocols/IGListAdapterMoveDelegate.html#/c:objc(pl)IGListAdapterMoveDelegate(im)listAdapter:moveObject:from:to:":{"name":"-listAdapter:moveObject:from:to:","abstract":"\u003cp\u003eAsks the delegate to move a section object as the result of interactive reordering.\u003c/p\u003e","parent_name":"IGListAdapterMoveDelegate"},"Protocols/IGListAdapterDelegate.html#/c:objc(pl)IGListAdapterDelegate(im)listAdapter:willDisplayObject:atIndex:":{"name":"-listAdapter:willDisplayObject:atIndex:","abstract":"\u003cp\u003eNotifies the delegate that a list object is about to be displayed.\u003c/p\u003e","parent_name":"IGListAdapterDelegate"},"Protocols/IGListAdapterDelegate.html#/c:objc(pl)IGListAdapterDelegate(im)listAdapter:didEndDisplayingObject:atIndex:":{"name":"-listAdapter:didEndDisplayingObject:atIndex:","abstract":"\u003cp\u003eNotifies the delegate that a list object is no longer being displayed.\u003c/p\u003e","parent_name":"IGListAdapterDelegate"},"Protocols/IGListAdapterDelegate.html#/c:objc(pl)IGListAdapterDelegate(im)listAdapter:willDisplayObject:cell:atIndexPath:":{"name":"-listAdapter:willDisplayObject:cell:atIndexPath:","abstract":"\u003cp\u003eNotifies the delegate that a list object is about to be displayed. This method is distinct from willDisplayObject:atIndex","parent_name":"IGListAdapterDelegate"},"Protocols/IGListAdapterDelegate.html#/c:objc(pl)IGListAdapterDelegate(im)listAdapter:didEndDisplayingObject:cell:atIndexPath:":{"name":"-listAdapter:didEndDisplayingObject:cell:atIndexPath:","abstract":"\u003cp\u003eNotifies the delegate that a list object is no longer being displayed. This method is distinct from didEndDisplayingObject:atIndex","parent_name":"IGListAdapterDelegate"},"Protocols/IGListAdapterDataSource.html#/c:objc(pl)IGListAdapterDataSource(im)objectsForListAdapter:":{"name":"-objectsForListAdapter:","abstract":"\u003cp\u003eAsks the data source for the objects to display in the list.\u003c/p\u003e","parent_name":"IGListAdapterDataSource"},"Protocols/IGListAdapterDataSource.html#/c:objc(pl)IGListAdapterDataSource(im)listAdapter:sectionControllerForObject:":{"name":"-listAdapter:sectionControllerForObject:","abstract":"\u003cp\u003eAsks the data source for a section controller for the specified object in the list.\u003c/p\u003e","parent_name":"IGListAdapterDataSource"},"Protocols/IGListAdapterDataSource.html#/c:objc(pl)IGListAdapterDataSource(im)emptyViewForListAdapter:":{"name":"-emptyViewForListAdapter:","abstract":"\u003cp\u003eAsks the data source for a view to use as the collection view background when the list is empty.\u003c/p\u003e","parent_name":"IGListAdapterDataSource"},"Protocols/IGListDiffable.html#/c:objc(pl)IGListDiffable(im)diffIdentifier":{"name":"-diffIdentifier","abstract":"\u003cp\u003eReturns a key that uniquely identifies the object.\u003c/p\u003e","parent_name":"IGListDiffable"},"Protocols/IGListDiffable.html#/c:objc(pl)IGListDiffable(im)isEqualToDiffableObject:":{"name":"-isEqualToDiffableObject:","abstract":"\u003cp\u003eReturns whether the receiver and a given object are equal.\u003c/p\u003e","parent_name":"IGListDiffable"},"Protocols/IGListDiffable.html":{"name":"IGListDiffable","abstract":"\u003cp\u003eThe \u003ccode\u003eIGListDiffable\u003c/code\u003e protocol provides methods needed to compare the identity and equality of two objects.\u003c/p\u003e"},"Protocols/IGListAdapterDataSource.html":{"name":"IGListAdapterDataSource","abstract":"\u003cp\u003eImplement this protocol to provide data to an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html\"\u003eIGListAdapter\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e"},"Protocols/IGListAdapterDelegate.html":{"name":"IGListAdapterDelegate","abstract":"\u003cp\u003eConform to \u003ccode\u003eIGListAdapterDelegate\u003c/code\u003e to receive display events for objects in a list.\u003c/p\u003e"},"Protocols/IGListAdapterMoveDelegate.html":{"name":"IGListAdapterMoveDelegate","abstract":"\u003cp\u003eConform to \u003ccode\u003eIGListAdapterMoveDelegate\u003c/code\u003e to receive interactive reordering requests.\u003c/p\u003e"},"Protocols/IGListAdapterPerformanceDelegate.html":{"name":"IGListAdapterPerformanceDelegate","abstract":"\u003cp\u003e\u003ccode\u003eIGListAdapterPerformanceDelegate\u003c/code\u003e can be used to measure cell dequeue, display, size, and scroll callbacks.\u003c/p\u003e"},"Protocols/IGListAdapterUpdateListener.html":{"name":"IGListAdapterUpdateListener","abstract":"\u003cp\u003eConform to this protocol to receive events about \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html\"\u003eIGListAdapter\u003c/a\u003e\u003c/code\u003e updates.\u003c/p\u003e"},"Protocols/IGListAdapterUpdaterDelegate.html":{"name":"IGListAdapterUpdaterDelegate","abstract":"\u003cp\u003eA protocol that receives events about \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapterUpdater.html\"\u003eIGListAdapterUpdater\u003c/a\u003e\u003c/code\u003e operations.\u003c/p\u003e"},"Protocols/IGListBatchContext.html":{"name":"IGListBatchContext","abstract":"\u003cp\u003eObjects conforming to the IGListBatchContext protocol provide a way for section controllers to mutate their cells or"},"Protocols/IGListBindable.html":{"name":"IGListBindable","abstract":"\u003cp\u003eA protocol for cells that configure themselves given a view model.\u003c/p\u003e"},"Protocols/IGListBindingSectionControllerDataSource.html":{"name":"IGListBindingSectionControllerDataSource","abstract":"\u003cp\u003eA protocol that returns data to power cells in an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListBindingSectionController.html\"\u003eIGListBindingSectionController\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e"},"Protocols/IGListBindingSectionControllerSelectionDelegate.html":{"name":"IGListBindingSectionControllerSelectionDelegate","abstract":"\u003cp\u003eA protocol that handles cell selection events in an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListBindingSectionController.html\"\u003eIGListBindingSectionController\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e"},"Protocols/IGListCollectionContext.html":{"name":"IGListCollectionContext","abstract":"\u003cp\u003eThe collection context provides limited access to the collection-related information that"},"Protocols/IGListCollectionViewDelegateLayout.html":{"name":"IGListCollectionViewDelegateLayout","abstract":"\u003cp\u003eConform to \u003ccode\u003eIGListCollectionViewDelegateLayout\u003c/code\u003e to provide customized layout information for a collection view.\u003c/p\u003e"},"Protocols/IGListCollectionViewLayoutCompatible.html":{"name":"IGListCollectionViewLayoutCompatible","abstract":"\u003cp\u003eA protocol for layouts that defines interaction with an IGListCollectionView, for recieving updated section indexes.\u003c/p\u003e"},"Protocols/IGListDisplayDelegate.html":{"name":"IGListDisplayDelegate","abstract":"\u003cp\u003eImplement this protocol to receive display events for a section controller when it is on screen.\u003c/p\u003e"},"Protocols/IGListScrollDelegate.html":{"name":"IGListScrollDelegate","abstract":"\u003cp\u003eImplement this protocol to receive display events for a section controller when it is on screen.\u003c/p\u003e"},"Protocols/IGListSingleSectionControllerDelegate.html":{"name":"IGListSingleSectionControllerDelegate","abstract":"\u003cp\u003eA delegate that can receive selection events on an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListSingleSectionController.html\"\u003eIGListSingleSectionController\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e"},"Protocols/IGListSupplementaryViewSource.html":{"name":"IGListSupplementaryViewSource","abstract":"\u003cp\u003eConform to this protocol to provide information about a list\u0026rsquo;s supplementary views. This data is used in"},"Protocols/IGListTransitionDelegate.html":{"name":"IGListTransitionDelegate","abstract":"\u003cp\u003eConform to \u003ccode\u003eIGListTransitionDelegate\u003c/code\u003e to provide customized layout information for a collection view.\u003c/p\u003e"},"Protocols/IGListUpdatingDelegate.html":{"name":"IGListUpdatingDelegate","abstract":"\u003cp\u003eImplement this protocol in order to handle both section and row based update events. Implementation should forward or"},"Protocols/IGListWorkingRangeDelegate.html":{"name":"IGListWorkingRangeDelegate","abstract":"\u003cp\u003eImplement this protocol to receive working range events for a list.\u003c/p\u003e"},"Enums/IGListAdapterUpdateType.html#/c:@E@IGListAdapterUpdateType@IGListAdapterUpdateTypePerformUpdates":{"name":"IGListAdapterUpdateTypePerformUpdates","abstract":"\u003cp\u003e\u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)performUpdatesAnimated:completion:\"\u003e-[IGListAdapter performUpdatesAnimated:completion:]\u003c/a\u003e\u003c/code\u003e was executed.\u003c/p\u003e","parent_name":"IGListAdapterUpdateType"},"Enums/IGListAdapterUpdateType.html#/c:@E@IGListAdapterUpdateType@IGListAdapterUpdateTypeReloadData":{"name":"IGListAdapterUpdateTypeReloadData","abstract":"\u003cp\u003e\u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)reloadDataWithCompletion:\"\u003e-[IGListAdapter reloadDataWithCompletion:]\u003c/a\u003e\u003c/code\u003e was executed.\u003c/p\u003e","parent_name":"IGListAdapterUpdateType"},"Enums/IGListAdapterUpdateType.html#/c:@E@IGListAdapterUpdateType@IGListAdapterUpdateTypeItemUpdates":{"name":"IGListAdapterUpdateTypeItemUpdates","abstract":"\u003cp\u003e\u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListCollectionContext.html#/c:objc(pl)IGListCollectionContext(im)performBatchAnimated:updates:completion:\"\u003e-[IGListCollectionContext performBatchAnimated:updates:completion:]\u003c/a\u003e\u003c/code\u003e was executed by an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListSectionController.html\"\u003eIGListSectionController\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapterUpdateType"},"Enums/IGListExperiment.html#/c:@E@IGListExperiment@IGListExperimentNone":{"name":"IGListExperimentNone","abstract":"\u003cp\u003eSpecifies no experiments.\u003c/p\u003e","parent_name":"IGListExperiment"},"Enums/IGListExperiment.html#/c:@E@IGListExperiment@IGListExperimentInvalidateLayoutForUpdates":{"name":"IGListExperimentInvalidateLayoutForUpdates","abstract":"\u003cp\u003eTest invalidating layout when cell reloads/updates in IGListBindingSectionController.\u003c/p\u003e","parent_name":"IGListExperiment"},"Enums/IGListExperiment.html#/c:@E@IGListExperiment@IGListExperimentThrowOnInconsistencyException":{"name":"IGListExperimentThrowOnInconsistencyException","abstract":"\u003cp\u003eThrow NSInternalInconsistencyException during an update\u003c/p\u003e","parent_name":"IGListExperiment"},"Enums/IGListExperiment.html#/c:@E@IGListExperiment@IGListExperimentRemoveDataSourceChangeEarlyExit":{"name":"IGListExperimentRemoveDataSourceChangeEarlyExit","abstract":"\u003cp\u003eRemove the early exit so multiple updates can\u0026rsquo;t happen at once\u003c/p\u003e","parent_name":"IGListExperiment"},"Enums/IGListExperiment.html#/c:@E@IGListExperiment@IGListExperimentFixPreferredFocusedView":{"name":"IGListExperimentFixPreferredFocusedView","abstract":"\u003cp\u003eAvoids creating off-screen cells\u003c/p\u003e","parent_name":"IGListExperiment"},"Enums/IGListDiffOption.html#/c:@E@IGListDiffOption@IGListDiffPointerPersonality":{"name":"IGListDiffPointerPersonality","abstract":"\u003cp\u003eCompare objects using pointer personality.\u003c/p\u003e","parent_name":"IGListDiffOption"},"Enums/IGListDiffOption.html#/c:@E@IGListDiffOption@IGListDiffEquality":{"name":"IGListDiffEquality","abstract":"\u003cp\u003eCompare objects using \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListDiffable.html#/c:objc(pl)IGListDiffable(im)isEqualToDiffableObject:\"\u003e-[IGListDiffable isEqualToDiffableObject:]\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListDiffOption"},"Enums/IGListDiffOption.html":{"name":"IGListDiffOption","abstract":"\u003cp\u003eAn option for how to do comparisons between similar objects.\u003c/p\u003e"},"Enums/IGListExperiment.html":{"name":"IGListExperiment","abstract":"\u003cp\u003eBitmask-able options used for pre-release feature testing.\u003c/p\u003e"},"Enums/IGListAdapterUpdateType.html":{"name":"IGListAdapterUpdateType","abstract":"\u003cp\u003eThe type of update that was performed by an \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html\"\u003eIGListAdapter\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e"},"Constants.html#/c:@IGListKitVersionNumber":{"name":"IGListKitVersionNumber","abstract":"\u003cp\u003eProject version number for IGListKit.\u003c/p\u003e"},"Constants.html#/c:@IGListKitVersionString":{"name":"IGListKitVersionString","abstract":"\u003cp\u003eProject version string for IGListKit.\u003c/p\u003e"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(im)initFromObjects:toObjects:toSectionControllers:":{"name":"-initFromObjects:toObjects:toSectionControllers:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(im)init":{"name":"-init","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(cm)new":{"name":"+new","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(py)fromObjects":{"name":"fromObjects","abstract":"\u003cp\u003eThe previous objects in the collection view. Objects must conform to \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListDiffable.html\"\u003eIGListDiffable\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(py)toObjects":{"name":"toObjects","abstract":"\u003cp\u003eThe new objects in the collection view. Objects must conform to \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListDiffable.html\"\u003eIGListDiffable\u003c/a\u003e\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(py)toSectionControllers":{"name":"toSectionControllers","abstract":"\u003cp\u003eThe section controllers corresponding to the \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListTransitionData.html#/c:objc(cs)IGListTransitionData(py)toObjects\"\u003etoObjects\u003c/a\u003e\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListTransitionData"},"Classes/IGListSingleSectionController.html#/c:objc(cs)IGListSingleSectionController(im)initWithCellClass:configureBlock:sizeBlock:":{"name":"-initWithCellClass:configureBlock:sizeBlock:","abstract":"\u003cp\u003eCreates a new section controller for a given cell type that will always have only one cell when present in a list.\u003c/p\u003e","parent_name":"IGListSingleSectionController"},"Classes/IGListSingleSectionController.html#/c:objc(cs)IGListSingleSectionController(im)initWithNibName:bundle:configureBlock:sizeBlock:":{"name":"-initWithNibName:bundle:configureBlock:sizeBlock:","abstract":"\u003cp\u003eCreates a new section controller for a given nib name and bundle that will always have only one cell when present in a list.\u003c/p\u003e","parent_name":"IGListSingleSectionController"},"Classes/IGListSingleSectionController.html#/c:objc(cs)IGListSingleSectionController(im)initWithStoryboardCellIdentifier:configureBlock:sizeBlock:":{"name":"-initWithStoryboardCellIdentifier:configureBlock:sizeBlock:","abstract":"\u003cp\u003eCreates a new section controller for a given storyboard cell identifier that will always have only one cell when present in a list.\u003c/p\u003e","parent_name":"IGListSingleSectionController"},"Classes/IGListSingleSectionController.html#/c:objc(cs)IGListSingleSectionController(py)selectionDelegate":{"name":"selectionDelegate","abstract":"\u003cp\u003eAn optional delegate that handles selection and deselection.\u003c/p\u003e","parent_name":"IGListSingleSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)numberOfItems":{"name":"-numberOfItems","abstract":"\u003cp\u003eReturns the number of items in the section.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)sizeForItemAtIndex:":{"name":"-sizeForItemAtIndex:","abstract":"\u003cp\u003eThe specific size for the item at the specified index.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)cellForItemAtIndex:":{"name":"-cellForItemAtIndex:","abstract":"\u003cp\u003eReturn a dequeued cell for a given index.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)didUpdateToObject:":{"name":"-didUpdateToObject:","abstract":"\u003cp\u003eUpdates the section controller to a new object.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)shouldSelectItemAtIndex:":{"name":"-shouldSelectItemAtIndex:","abstract":"\u003cp\u003eAsks the section controller if the cell at the specified index path should be selected\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)shouldDeselectItemAtIndex:":{"name":"-shouldDeselectItemAtIndex:","abstract":"\u003cp\u003eAsks the section controller if the cell at the specified index path should be deselected\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)didSelectItemAtIndex:":{"name":"-didSelectItemAtIndex:","abstract":"\u003cp\u003eTells the section controller that the cell at the specified index path was selected.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)didDeselectItemAtIndex:":{"name":"-didDeselectItemAtIndex:","abstract":"\u003cp\u003eTells the section controller that the cell at the specified index path was deselected.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)didHighlightItemAtIndex:":{"name":"-didHighlightItemAtIndex:","abstract":"\u003cp\u003eTells the section controller that the cell at the specified index path was highlighted.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)didUnhighlightItemAtIndex:":{"name":"-didUnhighlightItemAtIndex:","abstract":"\u003cp\u003eTells the section controller that the cell at the specified index path was unhighlighted.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)contextMenuConfigurationForItemAtIndex:point:":{"name":"-contextMenuConfigurationForItemAtIndex:point:","abstract":"\u003cp\u003eTells the section controller that the cell has requested a menu configuration.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)canMoveItemAtIndex:":{"name":"-canMoveItemAtIndex:","abstract":"\u003cp\u003eIdentifies whether an object can be moved through interactive reordering.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(im)moveObjectFromIndex:toIndex:":{"name":"-moveObjectFromIndex:toIndex:","abstract":"\u003cp\u003eNotifies the section that a list object should move within a section as the result of interactive reordering.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)viewController":{"name":"viewController","abstract":"\u003cp\u003eThe view controller housing the adapter that created this section controller.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)collectionContext":{"name":"collectionContext","abstract":"\u003cp\u003eA context object for interacting with the collection.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)section":{"name":"section","abstract":"\u003cp\u003eReturns the section within the list for this section controller.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)isFirstSection":{"name":"isFirstSection","abstract":"\u003cp\u003eReturns \u003ccode\u003eYES\u003c/code\u003e if the section controller is the first section in the list, \u003ccode\u003eNO\u003c/code\u003e otherwise.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)isLastSection":{"name":"isLastSection","abstract":"\u003cp\u003eReturns \u003ccode\u003eYES\u003c/code\u003e if the section controller is the last section in the list, \u003ccode\u003eNO\u003c/code\u003e otherwise.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)inset":{"name":"inset","abstract":"\u003cp\u003eThe margins used to lay out content in the section controller.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)minimumLineSpacing":{"name":"minimumLineSpacing","abstract":"\u003cp\u003eThe minimum spacing to use between rows of items.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)minimumInteritemSpacing":{"name":"minimumInteritemSpacing","abstract":"\u003cp\u003eThe minimum spacing to use between items in the same row.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)supplementaryViewSource":{"name":"supplementaryViewSource","abstract":"\u003cp\u003eThe supplementary view source for the section controller. Can be \u003ccode\u003enil\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)displayDelegate":{"name":"displayDelegate","abstract":"\u003cp\u003eAn object that handles display events for the section controller. Can be \u003ccode\u003enil\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)workingRangeDelegate":{"name":"workingRangeDelegate","abstract":"\u003cp\u003eAn object that handles working range events for the section controller. Can be \u003ccode\u003enil\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)scrollDelegate":{"name":"scrollDelegate","abstract":"\u003cp\u003eAn object that handles scroll events for the section controller. Can be \u003ccode\u003enil\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListSectionController.html#/c:objc(cs)IGListSectionController(py)transitionDelegate":{"name":"transitionDelegate","abstract":"\u003cp\u003eAn object that handles transition events for the section controller. Can be \u003ccode\u003enil\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListSectionController"},"Classes/IGListGenericSectionController.html#/c:objc(cs)IGListGenericSectionController(py)object":{"name":"object","abstract":"\u003cp\u003eThe object mapped to this section controller. Matches the object provided in","parent_name":"IGListGenericSectionController"},"Classes/IGListGenericSectionController.html#/c:objc(cs)IGListGenericSectionController(im)didUpdateToObject:":{"name":"-didUpdateToObject:","abstract":"\u003cp\u003eUpdates the section controller to a new object.\u003c/p\u003e","parent_name":"IGListGenericSectionController"},"Classes/IGListCollectionViewLayoutInvalidationContext.html#/c:objc(cs)IGListCollectionViewLayoutInvalidationContext(py)invalidateSupplementaryListAttributes":{"name":"invalidateSupplementaryListAttributes","abstract":"\u003cp\u003eFalse by default. If true, supplementary list item attributes (headers and footers) will be invalidated.\u003c/p\u003e","parent_name":"IGListCollectionViewLayoutInvalidationContext"},"Classes/IGListCollectionViewLayoutInvalidationContext.html#/c:objc(cs)IGListCollectionViewLayoutInvalidationContext(py)invalidateAllListAttributes":{"name":"invalidateAllListAttributes","abstract":"\u003cp\u003eFalse by default. If true, all list item attributes will be invalidated.\u003c/p\u003e","parent_name":"IGListCollectionViewLayoutInvalidationContext"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(py)scrollDirection":{"name":"scrollDirection","abstract":"\u003cp\u003eDirection in which layout will be scrollable; items will be flowed in the perpendicular direction, \u0026ldquo;newlining\u0026rdquo; when they","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(py)stickyHeaderYOffset":{"name":"stickyHeaderYOffset","abstract":"\u003cp\u003eSet this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(py)showHeaderWhenEmpty":{"name":"showHeaderWhenEmpty","abstract":"\u003cp\u003eSet this to \u003ccode\u003eYES\u003c/code\u003e to show sticky header when a section had no item. Default is \u003ccode\u003eNO\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(py)preserveLayoutCacheOnInvalidateLayout":{"name":"preserveLayoutCacheOnInvalidateLayout","abstract":"\u003cp\u003eSet this to \u003ccode\u003eYES\u003c/code\u003e to keep layout cache when invalidateLayout is called. Default is \u003ccode\u003eNO\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(im)initWithStickyHeaders:scrollDirection:topContentInset:stretchToEdge:":{"name":"-initWithStickyHeaders:scrollDirection:topContentInset:stretchToEdge:","abstract":"\u003cp\u003eCreate and return a new collection view layout.\u003c/p\u003e","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionViewLayout.html#/c:objc(cs)IGListCollectionViewLayout(im)initWithStickyHeaders:topContentInset:stretchToEdge:":{"name":"-initWithStickyHeaders:topContentInset:stretchToEdge:","abstract":"\u003cp\u003eCreate and return a new vertically scrolling collection view layout.\u003c/p\u003e","parent_name":"IGListCollectionViewLayout"},"Classes/IGListCollectionView.html#/c:objc(cs)IGListCollectionView(im)initWithFrame:listCollectionViewLayout:":{"name":"-initWithFrame:listCollectionViewLayout:","abstract":"\u003cp\u003eCreate a new view with an \u003ccode\u003eIGListcollectionViewLayout\u003c/code\u003e class or subclass.\u003c/p\u003e","parent_name":"IGListCollectionView"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)cellClass":{"name":"-cellClass","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)configureCell:withViewModel:":{"name":"-configureCell:withViewModel:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)sizeForViewModel:":{"name":"-sizeForViewModel:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)didSelectItemWithCell:":{"name":"-didSelectItemWithCell:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)didDeselectItemWithCell:":{"name":"-didDeselectItemWithCell:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)didHighlightItemWithCell:":{"name":"-didHighlightItemWithCell:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)didUnhighlightItemWithCell:":{"name":"-didUnhighlightItemWithCell:","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSingleSectionController.html#/c:objc(cs)IGListBindingSingleSectionController(im)isDisplayingCell":{"name":"-isDisplayingCell","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e","parent_name":"IGListBindingSingleSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(py)dataSource":{"name":"dataSource","abstract":"\u003cp\u003eA data source that transforms a top-level object into view models, and returns cells and sizes for given view models.\u003c/p\u003e","parent_name":"IGListBindingSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(py)selectionDelegate":{"name":"selectionDelegate","abstract":"\u003cp\u003eA delegate that receives selection events from cells in an \u003ccode\u003eIGListBindingSectionController\u003c/code\u003e instance.\u003c/p\u003e","parent_name":"IGListBindingSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(py)object":{"name":"object","abstract":"\u003cp\u003eThe object currently assigned to the section controller, if any.\u003c/p\u003e","parent_name":"IGListBindingSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(py)viewModels":{"name":"viewModels","abstract":"\u003cp\u003eThe array of view models created from the data source. Values are changed when the top-level object changes or by","parent_name":"IGListBindingSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(im)updateAnimated:completion:":{"name":"-updateAnimated:completion:","abstract":"\u003cp\u003eTells the section controller to query for new view models, diff the changes, and update its cells.\u003c/p\u003e","parent_name":"IGListBindingSectionController"},"Classes/IGListBindingSectionController.html#/c:objc(cs)IGListBindingSectionController(im)moveObjectFromIndex:toIndex:":{"name":"-moveObjectFromIndex:toIndex:","abstract":"\u003cp\u003eNotifies the section that a list object should move within a section as the result of interactive reordering.\u003c/p\u003e","parent_name":"IGListBindingSectionController"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)delegate":{"name":"delegate","abstract":"\u003cp\u003eThe delegate that receives events with data on the performance of a transition.\u003c/p\u003e","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)sectionMovesAsDeletesInserts":{"name":"sectionMovesAsDeletesInserts","abstract":"\u003cp\u003eA flag indicating if a section move should be treated as a section \u0026ldquo;delete, then insert\u0026rdquo; operation. This can be useful if you\u0026rsquo;re","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)singleItemSectionUpdates":{"name":"singleItemSectionUpdates","abstract":"\u003cp\u003eONLY used when there is N section, but each section only contains 1 item.","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)preferItemReloadsForSectionReloads":{"name":"preferItemReloadsForSectionReloads","abstract":"\u003cp\u003eA flag indicating that section reloads should be treated as item reloads, instead of converting them to \u0026ldquo;delete, then insert\u0026rdquo; operations.","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)allowsReloadingOnTooManyUpdates":{"name":"allowsReloadingOnTooManyUpdates","abstract":"\u003cp\u003eIf there\u0026rsquo;s more than 100 diff updates, fallback to using \u003ccode\u003ereloadData\u003c/code\u003e to avoid stalling the main thread.\u003c/p\u003e","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)allowsBackgroundDiffing":{"name":"allowsBackgroundDiffing","abstract":"\u003cp\u003eAllow the diffing to be performed on a background thread.\u003c/p\u003e","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)experiments":{"name":"experiments","abstract":"\u003cp\u003eA bitmask of experiments to conduct on the updater.\u003c/p\u003e","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)adaptiveDiffingExperimentConfig":{"name":"adaptiveDiffingExperimentConfig","abstract":"\u003cp\u003eThis is an experimental feature to customize how diffing is performed. If there’s clear value, we’ll make this a real API and make","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterUpdater.html#/c:objc(cs)IGListAdapterUpdater(py)adaptiveCoalescingExperimentConfig":{"name":"adaptiveCoalescingExperimentConfig","abstract":"\u003cp\u003eCustomize how coalescing works to speed up some updates\u003c/p\u003e","parent_name":"IGListAdapterUpdater"},"Classes/IGListAdapterDelegateAnnouncer.html#/c:objc(cs)IGListAdapterDelegateAnnouncer(cm)sharedInstance":{"name":"+sharedInstance","abstract":"\u003cp\u003eDefault announcer for all \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html\"\u003eIGListAdapter\u003c/a\u003e\u003c/code\u003e\u003c/p\u003e","parent_name":"IGListAdapterDelegateAnnouncer"},"Classes/IGListAdapterDelegateAnnouncer.html#/c:objc(cs)IGListAdapterDelegateAnnouncer(im)addListener:":{"name":"-addListener:","abstract":"\u003cp\u003eAdd a delegate that will receive callbacks for all \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListAdapter.html\"\u003eIGListAdapter\u003c/a\u003e\u003c/code\u003e.","parent_name":"IGListAdapterDelegateAnnouncer"},"Classes/IGListAdapterDelegateAnnouncer.html#/c:objc(cs)IGListAdapterDelegateAnnouncer(im)removeListener:":{"name":"-removeListener:","abstract":"\u003cp\u003eRemove delegate\u003c/p\u003e","parent_name":"IGListAdapterDelegateAnnouncer"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)viewController":{"name":"viewController","abstract":"\u003cp\u003eThe view controller that houses the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)collectionView":{"name":"collectionView","abstract":"\u003cp\u003eThe collection view used with the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)dataSource":{"name":"dataSource","abstract":"\u003cp\u003eThe object that acts as the data source for the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)delegate":{"name":"delegate","abstract":"\u003cp\u003eThe object that receives top-level events for section controllers.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)collectionViewDelegate":{"name":"collectionViewDelegate","abstract":"\u003cp\u003eThe object that receives \u003ccode\u003eUICollectionViewDelegate\u003c/code\u003e events.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)scrollViewDelegate":{"name":"scrollViewDelegate","abstract":"\u003cp\u003eThe object that receives \u003ccode\u003eUIScrollViewDelegate\u003c/code\u003e events.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)moveDelegate":{"name":"moveDelegate","abstract":"\u003cp\u003eThe object that receives \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListAdapterMoveDelegate.html\"\u003eIGListAdapterMoveDelegate\u003c/a\u003e\u003c/code\u003e events resulting from interactive reordering of sections.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)performanceDelegate":{"name":"performanceDelegate","abstract":"\u003cp\u003eThe object that receives \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListAdapterPerformanceDelegate.html\"\u003eIGListAdapterPerformanceDelegate\u003c/a\u003e\u003c/code\u003e events to measure performance.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)updater":{"name":"updater","abstract":"\u003cp\u003eThe updater for the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)experiments":{"name":"experiments","abstract":"\u003cp\u003eA bitmask of experiments to conduct on the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(py)autoDeselectEnabled":{"name":"autoDeselectEnabled","abstract":"\u003cp\u003eWhen true, we automatically deselect cells after they are selected (animated), so each section-controller doesn\u0026rsquo;t have to.","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)initWithUpdater:viewController:workingRangeSize:":{"name":"-initWithUpdater:viewController:workingRangeSize:","abstract":"\u003cp\u003eInitializes a new \u003ccode\u003eIGListAdapter\u003c/code\u003e object.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)initWithUpdater:viewController:":{"name":"-initWithUpdater:viewController:","abstract":"\u003cp\u003eInitializes a new \u003ccode\u003eIGListAdapter\u003c/code\u003e object with a working range of \u003ccode\u003e0\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)performUpdatesAnimated:completion:":{"name":"-performUpdatesAnimated:completion:","abstract":"\u003cp\u003ePerform an update from the previous state of the data source. This is analogous to calling","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)reloadDataWithCompletion:":{"name":"-reloadDataWithCompletion:","abstract":"\u003cp\u003ePerform an immediate reload of the data in the data source, discarding the old objects.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)reloadObjects:":{"name":"-reloadObjects:","abstract":"\u003cp\u003eReload the list for only the specified objects.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sectionControllerForSection:":{"name":"-sectionControllerForSection:","abstract":"\u003cp\u003eQuery the section controller at a given section index. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sectionForSectionController:":{"name":"-sectionForSectionController:","abstract":"\u003cp\u003eQuery the section index of a list. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sectionControllerForObject:":{"name":"-sectionControllerForObject:","abstract":"\u003cp\u003eReturns the section controller for the specified object. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)objectForSectionController:":{"name":"-objectForSectionController:","abstract":"\u003cp\u003eReturns the object corresponding to the specified section controller in the list. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)objectAtSection:":{"name":"-objectAtSection:","abstract":"\u003cp\u003eReturns the object corresponding to a section in the list. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sectionForObject:":{"name":"-sectionForObject:","abstract":"\u003cp\u003eReturns the section corresponding to the specified object in the list. Constant time lookup.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)objects":{"name":"-objects","abstract":"\u003cp\u003eReturns a copy of all the objects currently driving the adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)visibleSectionControllers":{"name":"-visibleSectionControllers","abstract":"\u003cp\u003eAn unordered array of the currently visible section controllers.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)visibleObjects":{"name":"-visibleObjects","abstract":"\u003cp\u003eAn unordered array of the currently visible objects.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)indexesOfVisibleObjects":{"name":"-indexesOfVisibleObjects","abstract":"\u003cp\u003eLess performant that \u003ccode\u003evisibleObjects\u003c/code\u003e but gives you an ordering to the list of visible objects in \u003ccode\u003eself.objects\u003c/code\u003e.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)visibleCellsForObject:":{"name":"-visibleCellsForObject:","abstract":"\u003cp\u003eAn unordered array of the currently visible cells for a given object.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:additionalOffset:animated:":{"name":"-scrollToObject:supplementaryKinds:scrollDirection:scrollPosition:additionalOffset:animated:","abstract":"\u003cp\u003eScrolls to the specified object in the list adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)indexPathForFirstVisibleItem":{"name":"-indexPathForFirstVisibleItem","abstract":"\u003cp\u003eReturns the index path for the first visible cell that has been scrolled to.","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)offsetForFirstVisibleItemWithScrollDirection:":{"name":"-offsetForFirstVisibleItemWithScrollDirection:","abstract":"\u003cp\u003eGets the scroll offset of the first visible cell scrolled into in the collection view.","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sizeForItemAtIndexPath:":{"name":"-sizeForItemAtIndexPath:","abstract":"\u003cp\u003eReturns the size of a cell at the specified index path.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)sizeForSupplementaryViewOfKind:atIndexPath:":{"name":"-sizeForSupplementaryViewOfKind:atIndexPath:","abstract":"\u003cp\u003eReturns the size of a supplementary view in the list at the specified index path.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)addUpdateListener:":{"name":"-addUpdateListener:","abstract":"\u003cp\u003eAdds a listener to the list adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListAdapter.html#/c:objc(cs)IGListAdapter(im)removeUpdateListener:":{"name":"-removeUpdateListener:","abstract":"\u003cp\u003eRemoves a listener from the list adapter.\u003c/p\u003e","parent_name":"IGListAdapter"},"Classes/IGListMoveIndexPath.html#/c:objc(cs)IGListMoveIndexPath(py)from":{"name":"from","abstract":"\u003cp\u003eAn index path in the old collection.\u003c/p\u003e","parent_name":"IGListMoveIndexPath"},"Classes/IGListMoveIndexPath.html#/c:objc(cs)IGListMoveIndexPath(py)to":{"name":"to","abstract":"\u003cp\u003eAn index path in the new collection.\u003c/p\u003e","parent_name":"IGListMoveIndexPath"},"Classes/IGListMoveIndex.html#/c:objc(cs)IGListMoveIndex(py)from":{"name":"from","abstract":"\u003cp\u003eAn index in the old collection.\u003c/p\u003e","parent_name":"IGListMoveIndex"},"Classes/IGListMoveIndex.html#/c:objc(cs)IGListMoveIndex(py)to":{"name":"to","abstract":"\u003cp\u003eAn index in the new collection.\u003c/p\u003e","parent_name":"IGListMoveIndex"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(py)inserts":{"name":"inserts","abstract":"\u003cp\u003eThe indexes inserted into the new collection.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(py)deletes":{"name":"deletes","abstract":"\u003cp\u003eThe indexes deleted from the old collection.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(py)updates":{"name":"updates","abstract":"\u003cp\u003eThe indexes in the old collection that need updated.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(py)moves":{"name":"moves","abstract":"\u003cp\u003eThe moves from an index in the old collection to an index in the new collection.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(py)hasChanges":{"name":"hasChanges","abstract":"\u003cp\u003eA Read-only boolean that indicates whether the result has any changes or not.","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(im)oldIndexForIdentifier:":{"name":"-oldIndexForIdentifier:","abstract":"\u003cp\u003eReturns the index of the object with the specified identifier \u003cem\u003ebefore\u003c/em\u003e the diff.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(im)newIndexForIdentifier:":{"name":"-newIndexForIdentifier:","abstract":"\u003cp\u003eReturns the index of the object with the specified identifier \u003cem\u003eafter\u003c/em\u003e the diff.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexSetResult.html#/c:objc(cs)IGListIndexSetResult(im)resultForBatchUpdates":{"name":"-resultForBatchUpdates","abstract":"\u003cp\u003eCreates a new result object with operations safe for use in \u003ccode\u003eUITableView\u003c/code\u003e and \u003ccode\u003eUICollectionView\u003c/code\u003e batch updates.\u003c/p\u003e","parent_name":"IGListIndexSetResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(py)inserts":{"name":"inserts","abstract":"\u003cp\u003eThe index paths inserted into the new collection.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(py)deletes":{"name":"deletes","abstract":"\u003cp\u003eThe index paths deleted from the old collection.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(py)updates":{"name":"updates","abstract":"\u003cp\u003eThe index paths in the old collection that need updated.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(py)moves":{"name":"moves","abstract":"\u003cp\u003eThe moves from an index path in the old collection to an index path in the new collection.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(py)hasChanges":{"name":"hasChanges","abstract":"\u003cp\u003eA Read-only boolean that indicates whether the result has any changes or not.","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(im)oldIndexPathForIdentifier:":{"name":"-oldIndexPathForIdentifier:","abstract":"\u003cp\u003eReturns the index path of the object with the specified identifier \u003cem\u003ebefore\u003c/em\u003e the diff.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(im)newIndexPathForIdentifier:":{"name":"-newIndexPathForIdentifier:","abstract":"\u003cp\u003eReturns the index path of the object with the specified identifier \u003cem\u003eafter\u003c/em\u003e the diff.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListIndexPathResult.html#/c:objc(cs)IGListIndexPathResult(im)resultForBatchUpdates":{"name":"-resultForBatchUpdates","abstract":"\u003cp\u003eCreates a new result object with operations safe for use in \u003ccode\u003eUITableView\u003c/code\u003e and \u003ccode\u003eUICollectionView\u003c/code\u003e batch updates.\u003c/p\u003e","parent_name":"IGListIndexPathResult"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)insertSections":{"name":"insertSections","abstract":"\u003cp\u003eSection insert indexes.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)deleteSections":{"name":"deleteSections","abstract":"\u003cp\u003eSection delete indexes.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)moveSections":{"name":"moveSections","abstract":"\u003cp\u003eSection moves.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)insertIndexPaths":{"name":"insertIndexPaths","abstract":"\u003cp\u003eItem insert index paths.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)deleteIndexPaths":{"name":"deleteIndexPaths","abstract":"\u003cp\u003eItem delete index paths.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)updateIndexPaths":{"name":"updateIndexPaths","abstract":"\u003cp\u003eItem update index paths.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(py)moveIndexPaths":{"name":"moveIndexPaths","abstract":"\u003cp\u003eItem moves.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html#/c:objc(cs)IGListBatchUpdateData(im)initWithInsertSections:deleteSections:moveSections:insertIndexPaths:deleteIndexPaths:updateIndexPaths:moveIndexPaths:":{"name":"-initWithInsertSections:deleteSections:moveSections:insertIndexPaths:deleteIndexPaths:updateIndexPaths:moveIndexPaths:","abstract":"\u003cp\u003eCreates a new batch update object with section and item operations.\u003c/p\u003e","parent_name":"IGListBatchUpdateData"},"Classes/IGListBatchUpdateData.html":{"name":"IGListBatchUpdateData","abstract":"\u003cp\u003eAn instance of \u003ccode\u003eIGListBatchUpdateData\u003c/code\u003e takes section indexes and item index paths"},"Classes/IGListIndexPathResult.html":{"name":"IGListIndexPathResult","abstract":"\u003cp\u003eA result object returned when diffing with sections.\u003c/p\u003e"},"Classes/IGListIndexSetResult.html":{"name":"IGListIndexSetResult","abstract":"\u003cp\u003eA result object returned when diffing with indexes.\u003c/p\u003e"},"Classes/IGListMoveIndex.html":{"name":"IGListMoveIndex","abstract":"\u003cp\u003eAn object representing a move between indexes.\u003c/p\u003e"},"Classes/IGListMoveIndexPath.html":{"name":"IGListMoveIndexPath","abstract":"\u003cp\u003eAn object representing a move between indexes.\u003c/p\u003e"},"Classes/IGListAdapter.html":{"name":"IGListAdapter","abstract":"\u003cp\u003e\u003ccode\u003eIGListAdapter\u003c/code\u003e objects provide an abstraction for feeds of objects in a \u003ccode\u003eUICollectionView\u003c/code\u003e by breaking each object"},"Classes/IGListAdapterDelegateAnnouncer.html":{"name":"IGListAdapterDelegateAnnouncer","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e"},"Classes/IGListAdapterUpdater.html":{"name":"IGListAdapterUpdater","abstract":"\u003cp\u003eAn \u003ccode\u003eIGListAdapterUpdater\u003c/code\u003e is a concrete type that conforms to \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListUpdatingDelegate.html\"\u003eIGListUpdatingDelegate\u003c/a\u003e\u003c/code\u003e."},"Classes/IGListBindingSectionController.html":{"name":"IGListBindingSectionController","abstract":"\u003cp\u003eThis section controller uses a data source to transform its \u0026ldquo;top level\u0026rdquo; object into an array of diffable view models."},"Classes/IGListBindingSingleSectionController.html":{"name":"IGListBindingSingleSectionController","abstract":"\u003cp\u003eSpecial section controller that only contains a single item, and it will apply the view model update during -didUpdateObject: call, usually happened inside -[UICollectionView performBatchUpdates:completion:].\u003c/p\u003e"},"Classes/IGListCollectionView.html":{"name":"IGListCollectionView","abstract":"\u003cp\u003eThis \u003ccode\u003eUICollectionView\u003c/code\u003e subclass allows for partial layout invalidation using \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbClasses/IGListCollectionViewLayout.html\"\u003eIGListCollectionViewLayout\u003c/a\u003e\u003c/code\u003e,"},"Classes/IGListCollectionViewLayout.html":{"name":"IGListCollectionViewLayout","abstract":"\u003cp\u003eThis UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and"},"Classes/IGListCollectionViewLayoutInvalidationContext.html":{"name":"IGListCollectionViewLayoutInvalidationContext","abstract":"\u003cp\u003eThe default invalidation context class used by IGListCollectionViewLayout.\u003c/p\u003e"},"Classes/IGListGenericSectionController.html":{"name":"IGListGenericSectionController","abstract":"\u003cp\u003eThis class adds a helper layer to \u003ccode\u003eIGListSectionController\u0010\u003c/code\u003e to automatically store a generic object in"},"Classes.html#/c:objc(cs)IGListReloadDataUpdater":{"name":"IGListReloadDataUpdater","abstract":"\u003cp\u003eAn \u003ccode\u003eIGListReloadDataUpdater\u003c/code\u003e is a concrete type that conforms to \u003ccode\u003e\u003ca href=\"36f8f5912051ae747ef441d6511ca4cbProtocols/IGListUpdatingDelegate.html\"\u003eIGListUpdatingDelegate\u003c/a\u003e\u003c/code\u003e."},"Classes/IGListSectionController.html":{"name":"IGListSectionController","abstract":"\u003cp\u003eThe base class for section controllers used in a list. This class is intended to be subclassed.\u003c/p\u003e"},"Classes/IGListSingleSectionController.html":{"name":"IGListSingleSectionController","abstract":"\u003cp\u003eThis section controller is meant to make building simple, single-cell lists easier. By providing the type of cell, a block"},"Classes/IGListTransitionData.html":{"name":"IGListTransitionData","abstract":"\u003cp\u003eContainer object that holds the data needed for an update.\u003c/p\u003e"},"Categories/UIViewController%28IGListAdapter%29.html#/c:objc(cs)UIViewController(im)associatedListAdapters":{"name":"-associatedListAdapters","abstract":"\u003cp\u003eAdapters that have this VC as their \u003ccode\u003eviewController\u003c/code\u003e\u003c/p\u003e","parent_name":"UIViewController(IGListAdapter)"},"Categories/UIViewController%28IGListAdapter%29.html":{"name":"UIViewController(IGListAdapter)","abstract":"\u003cp\u003eUndocumented\u003c/p\u003e"},"best-practices-and-faq.html":{"name":"Best Practices and FAQ"},"generating-your-models-using-remodel.html":{"name":"Generating your models using remodel"},"getting-started.html":{"name":"Getting Started"},"iglistdiffable-and-equality.html":{"name":"IGListDiffable and Equality"},"installation.html":{"name":"Installation"},"migration.html":{"name":"Migration"},"modeling-and-binding.html":{"name":"Modeling and Binding"},"vision.html":{"name":"VISION"},"working-with-core-data.html":{"name":"Working with Core Data"},"working-with-uicollectionview.html":{"name":"Working with UICollectionView"},"Guides.html":{"name":"Guides","abstract":"\u003cp\u003eThe following guides are available globally.\u003c/p\u003e"},"Categories.html":{"name":"Categories","abstract":"\u003cp\u003eThe following categories are available globally.\u003c/p\u003e"},"Classes.html":{"name":"Classes","abstract":"\u003cp\u003eThe following classes are available globally.\u003c/p\u003e"},"Constants.html":{"name":"Constants","abstract":"\u003cp\u003eThe following constants are available globally.\u003c/p\u003e"},"Enums.html":{"name":"Enumerations","abstract":"\u003cp\u003eThe following enumerations are available globally.\u003c/p\u003e"},"Protocols.html":{"name":"Protocols","abstract":"\u003cp\u003eThe following protocols are available globally.\u003c/p\u003e"},"Type%20Definitions.html":{"name":"Type Definitions","abstract":"\u003cp\u003eThe following type definitions are available globally.\u003c/p\u003e"},"Functions.html":{"name":"Functions","abstract":"\u003cp\u003eThe following functions are available globally.\u003c/p\u003e"},"Structs.html":{"name":"Structures","abstract":"\u003cp\u003eThe following structures are available globally.\u003c/p\u003e"}} ================================================ FILE: docs/undocumented.json ================================================ { "warnings": [ { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListAdapterDelegateAnnouncer.h", "line": 18, "symbol": "IGListAdapterDelegateAnnouncer", "symbol_kind": "sourcekitten.source.lang.objc.decl.class", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 29, "symbol": "IGListBindingSingleSectionController.-cellClass", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 32, "symbol": "IGListBindingSingleSectionController.-configureCell:withViewModel:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 35, "symbol": "IGListBindingSingleSectionController.-sizeForViewModel:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 38, "symbol": "IGListBindingSingleSectionController.-didSelectItemWithCell:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 41, "symbol": "IGListBindingSingleSectionController.-didDeselectItemWithCell:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 44, "symbol": "IGListBindingSingleSectionController.-didHighlightItemWithCell:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 47, "symbol": "IGListBindingSingleSectionController.-didUnhighlightItemWithCell:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListBindingSingleSectionController.h", "line": 49, "symbol": "IGListBindingSingleSectionController.-isDisplayingCell", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListTransitionData.h", "line": 27, "symbol": "IGListTransitionData.-initFromObjects:toObjects:toSectionControllers:", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListTransitionData.h", "line": 31, "symbol": "IGListTransitionData.-init", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.instance", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/IGListTransitionData.h", "line": 32, "symbol": "IGListTransitionData.+new", "symbol_kind": "sourcekitten.source.lang.objc.decl.method.class", "warning": "undocumented" }, { "file": "/Users/TiM/Developer/IGListKit/Source/IGListKit/UIViewController+IGListAdapter.h", "line": 14, "symbol": "UIViewController(IGListAdapter)", "symbol_kind": "sourcekitten.source.lang.objc.decl.category", "warning": "undocumented" } ], "source_directory": "/Users/TiM/Developer/IGListKit" } ================================================ FILE: docs/vision.html ================================================ VISION Reference

Vision

This document serves to outline the long term goals of IGListKit and act as a guidance when making decisions about features and issues.

Prioritizing Features & Fixes

IGListKit is a data-driven, list-building framework built, owned, and maintained by the engineering team at Instagram. Because IGListKit powers parts of the Instagram iOS app, we prioritize features and bugs towards those that affect Instagram. However the team recognizes the wide range of use-cases for IGListKit and wants to serve as broad an audience as possible without sacrificing our own needs.

Goals & Scope

The core goal of IGListKit is to build fast, stable, and data-driven lists in iOS applications. That scope includes things like:

  • UICollectionView and UITableView integrations
  • Data and state management
  • Diffing algorithms

While IGListKit uses specific tools, we do want to limit the reach of how we use those tools. We highly encourage people to explore solutions that fit their needs and will try to assist when possible. Examples of things beyond the scope of IGListKit:

  • Advanced/custom UICollectionViewLayouts
  • Sizing and layout (e.g. auto layout, estimated sizes)
  • Render and display pipelines
  • Integration with third-parties

Collaboration & Community

While IGListKit is an Instagram project, we want to give as much ownership and responsibility to the community as possible. We welcome everyone to become a collaborator on the project with whatever level of contribution you feel comfortable with.

We recognize that maintaining open source projects can be demanding, and often done in addition to other responsibilities. We have no expectation for the amount or frequency of contribution from anyone.

We also ask that you help keep our community welcoming and open.

Communication

GitHub Issues serve as the “source of truth” for all communication and decision-making about IGListKit. This keeps everything open and centralized. We will consider other forms of communication (Slack, Facebook Group, etc) once the scale of the project and/or community demands it.

================================================ FILE: docs/working-with-core-data.html ================================================ Working with Core Data Reference

Working with Core Data

This guide provides details on how to work with Core Data and IGListKit.

Background

The main difference in the setup and architecture of a Core Data and IGListKit application is the configuration of the model layer. Core Data operates with a mutable model layer, where objects are always passed by reference and the same instance is modified when an object is edited.

IGListKit requires an immutable model in order to correctly calculate the diffing between model snapshots and to correctly animate the UICollectionView.

In order to satisfy these prerequisites, Core Data NSManagedObjects should not be used directly as ListDiffable objects. Instead, a view model (or some sort of token object) should be used to mimic (or act as a placeholder for) the data that will be displayed in the collection view.

Further discussion

There are further discussions on this topic at #460, #461, #407.

Basic Setup

The basic setup for Core Data and IGListKit is the same as the normal setup that is found in the Getting Started Guide. The main difference will be in the setup of the model used in the IGListAdapterDataSource.

Working with view model

Creating a view model

Suppose the Core Data model consist of:

extension User {
    @NSManaged var firstName: String
    @NSManaged var lastName: String
    @NSManaged var address: String
    @NSManaged var someVariableNotNeededInUI: String
}

A ViewModel object will contain only the necessary information needed to build UI. The properties of the ViewModel will be immutable:

class UserViewModel: NSObject {
    let firstName: String
    let lastName: String
    let address: String
}

We recommend writing a helper method to translate Core Data objects into ViewModel objects:

extension UserViewModel {
    static func fromCoreData(user: User) -> UserViewModel {
        // - Note: For avoiding Core Data threading violation, the following code should be wrapped in a
        // user.managedObjectContext?.performAndWait {}
        return UserViewModel(firstName: user.firstName, lastName: user.lastName, address: user.lastName)
    }
}

The IGListDiffable protocol is implemented on the ViewModel layer:

extension UserViewModel: ListDiffable {

    public func diffIdentifier() -> NSObjectProtocol {
        return NSString(string: firstName + lastName)
    }

    public func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let toObject = object as? UserViewModel else { return false }

        return self.firstName == toObject.firstName
            && self.lastName == toObject.lastName
            && self.address == toObject.address
    }
}

Setting up the view model in the adapter data source

Steps to configure the UICollectionView with the ViewModel:

  • Retrieve Core Data objects
  • Transform Core Data objects into ViewModel objects and return them
  • Track changes to Core Data objects and update the datasource with them

Retrieve Core Data objects

The way objects are retrieved from Core Data is depends on the project.

Example: Suppose there is a delegate Provider class with the role of fetching Core Data objects and checking for updates. It can use an NSFetchedResultsController to leverage on the Core Data framework and rely on automatic notifications for updates.

final class UserProvider: NSObject {

    private lazy var userFetchResultController: NSFetchedResultsController<User> = {
        let fetchRequest: NSFetchRequest<User> = NSFetchRequest(entityName: "User")

        // sort descriptors and predicates 
        // ...

        let fetchResultController = NSFetchedResultsController(
           fetchRequest: tripsFetchRequest,
           managedObjectContext: self.coreDataStack.mainQueueManagedObjectContext,
           sectionNameKeyPath: nil,
           cacheName: nil)

        // Set delegate to track CoreData changes
        fetchResultController.delegate = self

        return fetchResultController
    }()

    init(coreDataStack: CoreDataStack) {
        self.coreDataStack = coreDataStack
        super.init()
        do {
            try userFetchResultController.performFetch()
        }
        catch {
            fatalError("Cannot Fetch! \(error)")
        }
    }
}

Transform Core Data objects into view models

func getUsers() -> [UserViewModel]? {
    guard let users = self.userFetchResultController.fetchedObjects else { return nil }
    // Here we transform and return ViewModel objects!
    return users.flatMap { UserViewModel.fromCoreData(user: $0) }
}

Track changes to Core Data

The Provider will track changes to the Core Data model by listening to the NSFetchedResultsController methods and inform the application about this changes via KVO, notifications, delegation, etc.

extension UserProvider: NSFetchedResultsControllerDelegate {
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        self.delegate?.performUpdatesForCoreDataChange(animated: true)
    }
}

Configure the datasource

The data source retrieves ViewModels and configures the IGListSectionController with them:

func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return self.userProvider.getUsers()
}

Reacting to Core Data changes in UI

The UIViewController containing the UICollectionView, will react to the NSFetchedResultController messages by updating the UI:

func performUpdatesForCoreDataChange(animated: Bool) {
    // Updating contents of collection view
    self.adapter.performUpdates(animated: animated)
}
================================================ FILE: docs/working-with-uicollectionview.html ================================================ Working with UICollectionView Reference

Working with UICollectionView

This guide provides details on how to work with UICollectionView and IGListKit.

Background

Early versions of IGListKit (2.x and prior) shipped with a subclass of UICollectionView called IGListCollectionView. The class contained no special functionality and was merely used to enforce compile-time restrictions to prevent users from calling certain methods directly on UICollectionView. Beginning with 3.0, IGListCollectionView was removed for a number of reasons.

For further discussion see #240 and #409.

Methods to avoid

One of the primary purposes of IGListKit is to perform optimal batch updates for UICollectionView. Thus, clients should never call any APIs on UICollectionView that involved reloading, inserting, deleting, or otherwise updating cells and index paths. Instead, use the APIs provided by IGListAdapter. You should also avoid setting the delegate and dataSource of the collection view, as this is also the responsibility of IGListAdapter.

Avoid calling the following methods:

- (void)performBatchUpdates:(void (^)(void))updates
                 completion:(void (^)(BOOL))completion;

- (void)reloadData;

- (void)reloadSections:(NSIndexSet *)sections;

- (void)insertSections:(NSIndexSet *)sections;

- (void)deleteSections:(NSIndexSet *)sections;

- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;

- (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;

- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;

- (void)setDelegate:(id<UICollectionViewDelegate>)delegate;

- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource;

- (void)setBackgroundView:(UIView *)backgroundView;

Performance

In iOS 10, a new cell prefetching API was introduced. At Instagram, enabling this feature substantially degraded scrolling performance. We recommend setting isPrefetchingEnabled to NO (false in Swift). Note that the default value is true.

You can set this globally using UIAppearance:

if ([[UICollectionView class] instancesRespondToSelector:@selector(setPrefetchingEnabled:)]) {
    [[UICollectionView appearance] setPrefetchingEnabled:NO];
}
if #available(iOS 10, *) {
    UICollectionView.appearance().isPrefetchingEnabled = false
}
================================================ FILE: remodel-plugin/features/iglistdiffable.feature ================================================ # Copyright (c) Meta Platforms, Inc. and affiliates. All Rights Reserved. Feature: Outputting Value Objects implementing IGListDiffable @announce Scenario: Generating a value object, which correctly implements IGListDiffable using the specified diffIdentifier Given a file named "project/values/IGListDiffableTest.value" with: """ IGListDiffableTest includes(IGListDiffable) { CGRect someRect %diffIdentifier NSString *stringOne } """ When I run `../../bin/generate project` Then the file "project/values/IGListDiffableTest.h" should contain: """ #import #import #import @interface IGListDiffableTest : NSObject @property (nonatomic, readonly) CGRect someRect; @property (nonatomic, readonly, copy) NSString *stringOne; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithSomeRect:(CGRect)someRect stringOne:(NSString *)stringOne NS_DESIGNATED_INITIALIZER; @end """ And the file "project/values/IGListDiffableTest.m" should contain: """ - (id)diffIdentifier { return _stringOne; } """ And the file "project/values/IGListDiffableTest.m" should contain: """ - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } """ Scenario: Generating a value object, which correctly implements IGListDiffable using a CGRect property Given a file named "project/values/IGListDiffableTest2.value" with: """ IGListDiffableTest2 includes(IGListDiffable) { %diffIdentifier CGRect someRect } """ When I run `../../bin/generate project` Then the file "project/values/IGListDiffableTest2.h" should contain: """ #import #import #import @interface IGListDiffableTest2 : NSObject @property (nonatomic, readonly) CGRect someRect; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithSomeRect:(CGRect)someRect NS_DESIGNATED_INITIALIZER; @end """ And the file "project/values/IGListDiffableTest2.m" should contain: """ - (id)diffIdentifier { return [NSValue valueWithCGRect:_someRect]; } """ And the file "project/values/IGListDiffableTest2.m" should contain: """ - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } """ Scenario: Generating a value object, which correctly implements IGListDiffable using an NSInteger property Given a file named "project/values/IGListDiffableTest3.value" with: """ IGListDiffableTest3 includes(IGListDiffable) { %diffIdentifier NSInteger count } """ When I run `../../bin/generate project` Then the file "project/values/IGListDiffableTest3.m" should contain: """ - (id)diffIdentifier { return @(_count); } """ And the file "project/values/IGListDiffableTest3.m" should contain: """ - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } """ Scenario: Generating a value object, which correctly implements IGListDiffable defaulting to self as diffIdentifier Given a file named "project/values/IGListDiffableTest4.value" with: """ IGListDiffableTest4 includes(IGListDiffable) { CGRect someRect } """ When I run `../../bin/generate project` Then the file "project/values/IGListDiffableTest4.h" should contain: """ #import #import #import @interface IGListDiffableTest4 : NSObject @property (nonatomic, readonly) CGRect someRect; + (instancetype)new NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithSomeRect:(CGRect)someRect NS_DESIGNATED_INITIALIZER; @end """ And the file "project/values/IGListDiffableTest4.m" should contain: """ - (id)diffIdentifier { return self; } """ And the file "project/values/IGListDiffableTest4.m" should contain: """ - (BOOL)isEqualToDiffableObject:(nullable id)object { return [self isEqual:object]; } """ ================================================ FILE: remodel-plugin/src/__tests__/plugins/iglistdiffable-test.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ /// /// import * as IGListDiffable from '../../plugins/iglistdiffable'; import * as Error from '../../error'; import * as Maybe from '../../maybe'; import * as ObjC from '../../objc'; import * as ObjectSpec from '../../object-spec'; import * as ObjectGeneration from '../../object-generation'; const ObjectSpecPlugin = IGListDiffable.createPlugin(); function igListDiffableIsEqualMethod(): ObjC.Method { return { preprocessors: [], belongsToProtocol: Maybe.Just('IGListDiffable'), code: ['return [self isEqual:object];'], comments: [], compilerAttributes: [], keywords: [ { name: 'isEqualToDiffableObject', argument: Maybe.Just({ name: 'object', modifiers: [ObjC.KeywordArgumentModifier.Nullable()], type: { name: 'id', reference: 'id', }, }), }, ], returnType: { type: Maybe.Just({ name: 'BOOL', reference: 'BOOL', }), modifiers: [], }, }; } function igListDiffableDiffIdentifierMethodWithCode(code: string): ObjC.Method { return { preprocessors: [], belongsToProtocol: Maybe.Just('IGListDiffable'), code: [code], comments: [], compilerAttributes: [], keywords: [ { name: 'diffIdentifier', argument: Maybe.Nothing(), }, ], returnType: { type: Maybe.Just({ name: 'NSObject', reference: 'id', }), modifiers: [], }, }; } describe('ObjectSpecPlugins.IGListDiffable', function() { describe('Value Object', function() { describe('#instanceMethods', function() { it('returns two instance methods and uses self as diffIdentifier with no input attributes', function() { const objectType: ObjectSpec.Type = { annotations: {}, attributes: [], comments: [], excludes: [], includes: [], libraryName: Maybe.Nothing(), typeLookups: [], typeName: 'Foo', }; const instanceMethods: ObjC.Method[] = ObjectSpecPlugin.instanceMethods( objectType, ); const expectedInstanceMethods: ObjC.Method[] = [ igListDiffableIsEqualMethod(), igListDiffableDiffIdentifierMethodWithCode('return self;'), ]; expect(instanceMethods).toEqualJSON(expectedInstanceMethods); }); it('returns NSObjects directly as diffIdentifier', function() { const objectType: ObjectSpec.Type = { annotations: {}, attributes: [ { annotations: { diffIdentifier: [], }, comments: [], name: 'name', nullability: ObjC.Nullability.Inherited(), type: { fileTypeIsDefinedIn: Maybe.Nothing(), libraryTypeIsDefinedIn: Maybe.Nothing(), name: 'NSString', reference: 'NSString *', underlyingType: Maybe.Just('NSObject'), conformingProtocol: Maybe.Nothing(), }, }, ], comments: [], excludes: [], includes: [], libraryName: Maybe.Nothing(), typeLookups: [], typeName: 'Foo', }; const instanceMethods: ObjC.Method[] = ObjectSpecPlugin.instanceMethods( objectType, ); const expectedInstanceMethods: ObjC.Method[] = [ igListDiffableIsEqualMethod(), igListDiffableDiffIdentifierMethodWithCode('return _name;'), ]; expect(instanceMethods).toEqualJSON(expectedInstanceMethods); }); it('returns NSInteger as formatString as diffIdentifier', function() { const objectType: ObjectSpec.Type = { annotations: {}, attributes: [ { annotations: { diffIdentifier: [], }, comments: [], name: 'age', nullability: ObjC.Nullability.Inherited(), type: { fileTypeIsDefinedIn: Maybe.Nothing(), libraryTypeIsDefinedIn: Maybe.Nothing(), name: 'NSInteger', reference: 'NSInteger', underlyingType: Maybe.Nothing(), conformingProtocol: Maybe.Nothing(), }, }, ], comments: [], excludes: [], includes: [], libraryName: Maybe.Nothing(), typeLookups: [], typeName: 'Foo', }; const instanceMethods: ObjC.Method[] = ObjectSpecPlugin.instanceMethods( objectType, ); const expectedInstanceMethods: ObjC.Method[] = [ igListDiffableIsEqualMethod(), igListDiffableDiffIdentifierMethodWithCode('return @(_age);'), ]; expect(instanceMethods).toEqualJSON(expectedInstanceMethods); }); it('returns CGRect as string as diffIdentifier', function() { const objectType: ObjectSpec.Type = { annotations: {}, attributes: [ { annotations: { diffIdentifier: [], }, comments: [], name: 'rect', nullability: ObjC.Nullability.Inherited(), type: { fileTypeIsDefinedIn: Maybe.Nothing(), libraryTypeIsDefinedIn: Maybe.Nothing(), name: 'CGRect', reference: 'CGRect', underlyingType: Maybe.Nothing(), conformingProtocol: Maybe.Nothing(), }, }, ], comments: [], excludes: [], includes: [], libraryName: Maybe.Nothing(), typeLookups: [], typeName: 'Foo', }; const instanceMethods: ObjC.Method[] = ObjectSpecPlugin.instanceMethods( objectType, ); const expectedInstanceMethods: ObjC.Method[] = [ igListDiffableIsEqualMethod(), igListDiffableDiffIdentifierMethodWithCode( 'return [NSValue valueWithCGRect:_rect];', ), ]; expect(instanceMethods).toEqualJSON(expectedInstanceMethods); }); it('returns property marked with %diffIdentifier as diffIdentifier', function() { const objectType: ObjectSpec.Type = { annotations: {}, attributes: [ { annotations: {}, comments: [], name: 'name', nullability: ObjC.Nullability.Inherited(), type: { fileTypeIsDefinedIn: Maybe.Nothing(), libraryTypeIsDefinedIn: Maybe.Nothing(), name: 'NSString', reference: 'NSString *', underlyingType: Maybe.Just('NSObject'), conformingProtocol: Maybe.Nothing(), }, }, { annotations: { diffIdentifier: [], }, comments: [], name: 'age', nullability: ObjC.Nullability.Inherited(), type: { fileTypeIsDefinedIn: Maybe.Nothing(), libraryTypeIsDefinedIn: Maybe.Nothing(), name: 'NSInteger', reference: 'NSInteger', underlyingType: Maybe.Nothing(), conformingProtocol: Maybe.Nothing(), }, }, ], comments: [], excludes: [], includes: [], libraryName: Maybe.Nothing(), typeLookups: [], typeName: 'Foo', }; const instanceMethods: ObjC.Method[] = ObjectSpecPlugin.instanceMethods( objectType, ); const expectedInstanceMethods: ObjC.Method[] = [ igListDiffableIsEqualMethod(), igListDiffableDiffIdentifierMethodWithCode('return @(_age);'), ]; expect(instanceMethods).toEqualJSON(expectedInstanceMethods); }); }); }); }); ================================================ FILE: remodel-plugin/src/plugins/iglistdiffable-utils.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import * as Maybe from '../maybe'; import * as ObjC from '../objc'; import * as ObjCTypeUtils from '../objc-type-utils'; import * as ObjectSpec from '../object-spec'; import * as ObjectSpecCodeUtils from '../object-spec-code-utils'; function isEqualToDiffableObjectMethod(): ObjC.Method { return { preprocessors: [], belongsToProtocol: Maybe.Just('IGListDiffable'), code: ['return [self isEqual:object];'], comments: [], compilerAttributes: [], keywords: [ { name: 'isEqualToDiffableObject', argument: Maybe.Just({ name: 'object', modifiers: [ObjC.KeywordArgumentModifier.Nullable()], type: { name: 'id', reference: 'id', }, }), }, ], returnType: { type: Maybe.Just({ name: 'BOOL', reference: 'BOOL', }), modifiers: [], }, }; } function functionReturnValueForIvarWithFunctionName( iVarString: string, functionToCall: string, ): string { return functionToCall + '(' + iVarString + ')'; } function formattedStringValueForIvarWithFormatSpecifier( iVarString: string, stringFormatSpecifier: string, optionalCast: string = null, ): string { var castString: string = optionalCast === null ? '' : '(' + optionalCast + ')'; return ( '[NSString stringWithFormat:@"' + stringFormatSpecifier + '", ' + castString + iVarString + ']' ); } function nullableObjectValueWithFallback( objectValue: string, optionalFallback: string = null, ) { return optionalFallback === null ? objectValue : `${objectValue} ?: ${optionalFallback}`; } function wrappedInNSValueForTypeName(iVarString: string, typeName: string) { return `[NSValue valueWith${typeName}:${iVarString}]`; } function objectValueForAttribute( attribute: ObjectSpec.Attribute, optionalFallback: string = null, ): string { const iVarString: string = ObjectSpecCodeUtils.ivarForAttribute(attribute); const type: ObjC.Type = ObjectSpecCodeUtils.computeTypeOfAttribute(attribute); return ObjCTypeUtils.matchType( { id: function() { return formattedStringValueForIvarWithFormatSpecifier(iVarString, '%@'); }, NSObject: function() { return nullableObjectValueWithFallback(iVarString, optionalFallback); }, BOOL: function() { return `@(${iVarString})`; }, NSInteger: function() { return `@(${iVarString})`; }, NSUInteger: function() { return `@(${iVarString})`; }, double: function() { return `@(${iVarString})`; }, float: function() { return `@(${iVarString})`; }, CGFloat: function() { return `@(${iVarString})`; }, NSTimeInterval: function() { return `@(${iVarString})`; }, uintptr_t: function() { return `@(${iVarString})`; }, uint32_t: function() { return `@(${iVarString})`; }, uint64_t: function() { return `@(${iVarString})`; }, int32_t: function() { return `@(${iVarString})`; }, int64_t: function() { return `@(${iVarString})`; }, SEL: function() { return functionReturnValueForIvarWithFunctionName( iVarString, 'NSStringFromSelector', ); }, NSRange: function() { return wrappedInNSValueForTypeName(iVarString, 'Range'); }, CGRect: function() { return wrappedInNSValueForTypeName(iVarString, type.name); }, CGPoint: function() { return wrappedInNSValueForTypeName(iVarString, type.name); }, CGSize: function() { return wrappedInNSValueForTypeName(iVarString, type.name); }, UIEdgeInsets: function() { return wrappedInNSValueForTypeName(iVarString, type.name); }, Class: function() { return formattedStringValueForIvarWithFormatSpecifier(iVarString, '%@'); }, dispatch_block_t: function() { return formattedStringValueForIvarWithFormatSpecifier(iVarString, '%@'); }, unmatchedType: function() { return nullableObjectValueWithFallback('self', optionalFallback); }, }, type, ); } export {isEqualToDiffableObjectMethod, objectValueForAttribute}; ================================================ FILE: remodel-plugin/src/plugins/iglistdiffable.ts ================================================ /** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import * as Code from '../code'; import * as Error from '../error'; import * as FileWriter from '../file-writer'; import * as IGListDiffableUtils from './iglistdiffable-utils'; import * as Maybe from '../maybe'; import * as ObjC from '../objc'; import * as ObjectSpec from '../object-spec'; function diffIdentiferAttributeFilter( attribute: ObjectSpec.Attribute, index, array, ): boolean { return attribute.annotations['diffIdentifier'] != null; } function diffIdentifierMethodImplementation( objectType: ObjectSpec.Type, ): string[] { const diffIdentifierAttributes: ObjectSpec.Attribute[] = objectType.attributes.filter( diffIdentiferAttributeFilter, ); if (diffIdentifierAttributes.length > 0) { // use first marked attribute as identifier, if available return [ 'return ' + IGListDiffableUtils.objectValueForAttribute( diffIdentifierAttributes[0], ) + ';', ]; } else { // fallback/default to self return ['return self;']; } } function diffIdentifierMethod(objectType: ObjectSpec.Type): ObjC.Method { return { preprocessors: [], belongsToProtocol: Maybe.Just('IGListDiffable'), code: diffIdentifierMethodImplementation(objectType), comments: [], compilerAttributes: [], keywords: [ { name: 'diffIdentifier', argument: Maybe.Nothing(), }, ], returnType: { type: Maybe.Just({ name: 'NSObject', reference: 'id', }), modifiers: [], }, }; } export function createPlugin(): ObjectSpec.Plugin { return { additionalFiles: function(objectType: ObjectSpec.Type): Code.File[] { return []; }, transformBaseFile: function( objectType: ObjectSpec.Type, baseFile: Code.File, ): Code.File { return baseFile; }, additionalTypes: function(objectType: ObjectSpec.Type): ObjectSpec.Type[] { return []; }, attributes: function(objectType: ObjectSpec.Type): ObjectSpec.Attribute[] { return []; }, classMethods: function(objectType: ObjectSpec.Type): ObjC.Method[] { return []; }, transformFileRequest: function( request: FileWriter.Request, ): FileWriter.Request { return request; }, fileType: function( objectType: ObjectSpec.Type, ): Maybe.Maybe { return Maybe.Nothing(); }, forwardDeclarations: function( objectType: ObjectSpec.Type, ): ObjC.ForwardDeclaration[] { return []; }, functions: function(objectType: ObjectSpec.Type): ObjC.Function[] { return []; }, headerComments: function(objectType: ObjectSpec.Type): ObjC.Comment[] { return []; }, implementedProtocols: function( objectType: ObjectSpec.Type, ): ObjC.Protocol[] { return [{name: 'IGListDiffable'}]; }, imports: function(objectType: ObjectSpec.Type): ObjC.Import[] { return [ { file: 'IGListDiffable.h', isPublic: true, requiresCPlusPlus: false, library: Maybe.Just('IGListKit'), }, ]; }, instanceMethods: function(objectType: ObjectSpec.Type): ObjC.Method[] { return [ IGListDiffableUtils.isEqualToDiffableObjectMethod(), diffIdentifierMethod(objectType), ]; }, macros: function(valueType: ObjectSpec.Type): ObjC.Macro[] { return []; }, properties: function(objectType: ObjectSpec.Type): ObjC.Property[] { return []; }, requiredIncludesToRun: ['IGListDiffable'], staticConstants: function(objectType: ObjectSpec.Type): ObjC.Constant[] { return []; }, validationErrors: function(objectType: ObjectSpec.Type): Error.Error[] { return []; }, nullability: function( objectType: ObjectSpec.Type, ): Maybe.Maybe { return Maybe.Nothing(); }, subclassingRestricted: function(objectType: ObjectSpec.Type): boolean { return false; }, }; } ================================================ FILE: scripts/build_docs.sh ================================================ #!/bin/bash # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. if ! which jazzy >/dev/null; then echo "Jazzy not detected: You can download it from https://github.com/realm/jazzy" exit fi # Docs by jazzy # https://github.com/realm/jazzy # ------------------------------ SOURCE=Source SOURCE_TMP=IGListKit SOURCEDIR=Source/ jazzy \ --objc \ --clean \ --author 'Instagram' \ --author_url 'https://twitter.com/MetaOpenSource' \ --github_url 'https://github.com/Instagram/IGListKit' \ --sdk iphonesimulator \ --module 'IGListKit' \ --framework-root $SOURCEDIR/ \ --umbrella-header $SOURCEDIR/$SOURCE_TMP/IGListKit.h \ --readme README.md \ --documentation "Guides/*.md" \ --output docs/ ================================================ FILE: scripts/generate_spm_sources_layout.sh ================================================ #!/bin/sh # Swift Package Manager [has some strict requirements](https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#creating-c-language-targets) to source files layout. # We can gain SPM support with minimal project structure changes by applying an autogenerated layout based on symbolic links technics. # SPM related files located under `spm` folder at project's root. # You should **NEVER** modify it manually nor inlude to Xcode's project. # ### Generate SPM layout # 1. From **project's root** run: # `bash scripts/generate_spm_sources_layout.sh` # 2. Commit Changes # Repeate those steps each time you delete/add project's files. **Make sure** to have this CI step which will check that `generate_spm_sources_layout.sh` is not broken. set -e ###IGListDiffKit SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH="spm/Sources/IGListDiffKit/include" SPM_IG_LIST_DIFF_KIT_SOURCES_PATH="spm/Sources/IGListDiffKit" function generate_ig_list_diff_kit_spm_public_headers() { echo "Generate symbolic links for all public headers. *.h" echo "Generated under ${SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH}" public_headers_list=$( find "Source/IGListDiffKit" \ -type f -name "*.[h]" \ -not -path "spm/*" \ -not -path "Source/IGListDiffKit/Internal/*" \ -not -path "Examples/*" | sed "s| \([^/]\)|:\1|g" ) SRC_ROOT=$(pwd) cd $SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH for public_file in $public_headers_list; do file_to_link=$(echo $public_file | sed "s|:| |g") ln -s ../../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_diff_kit_spm_private_headers() { echo "Generate symbolic links for all private headers *.h" echo "Generated under ${SPM_IG_LIST_DIFF_KIT_SOURCES_PATH}" private_headers_list=$( find "Source/IGListDiffKit/Internal" \ -type f -name "*.h" \ -not -path "spm/*" | sed "s| \([^/]\)|:\1|g") SRC_ROOT=$(pwd) cd $SPM_IG_LIST_DIFF_KIT_SOURCES_PATH for private_file in $private_headers_list; do file_to_link=$(echo $private_file | sed "s|:| |g") ln -s ../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_diff_kit_spm_sources() { echo "Generate symbolic links for all public implementations. *.m & *.mm" echo "Generated under ${SPM_IG_LIST_DIFF_KIT_SOURCES_PATH}" sources_list=$( find "Source/IGListDiffKit" \ -type f -name "*.[m]" -o -name "*.mm" \ -not -path "spm/*" | sed "s| \([^/]\)|:\1|g") SRC_ROOT=$(pwd) cd $SPM_IG_LIST_DIFF_KIT_SOURCES_PATH for source_file in $sources_list; do file_to_link=$(echo $source_file | sed "s|:| |g") ln -s ../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_diff_kit() { generate_ig_list_diff_kit_spm_public_headers generate_ig_list_diff_kit_spm_private_headers generate_ig_list_diff_kit_spm_sources } #### IGListKit SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH="spm/Sources/IGListKit/include" SPM_IG_LIST_KIT_SOURCES_PATH="spm/Sources/IGListKit" function generate_ig_list_kit_spm_public_headers() { echo "Generate symbolic links for all public headers. *.h" echo "Generated under ${SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH}" public_headers_list=$( find "Source/IGListKit" \ -name "*.[h]" \ -type f -not -path "spm/*" \ -not -path "Source/IGListKit/Internal/*" \ -not -path "Examples/*" | sed "s| \([^/]\)|:\1|g" ) SRC_ROOT=$(pwd) cd $SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH for public_file in $public_headers_list; do file_to_link=$(echo $public_file | sed "s|:| |g") ln -s ../../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_kit_spm_private_headers() { echo "Generate symbolic links for all private headers/implementations *.h && *.m" echo "Generated under ${SPM_IG_LIST_KIT_SOURCES_PATH}" shared_ig_diff_kit_sorces_list=$(find "Source/IGListDiffKit/Internal" \ -name "*.[hm]" -o -name "*.mm" \ -type f -not -path "spm/*" \ -not -path "Examples/*" | sed "s| \([^/]\)|:\1|g") private_headers_list=$(find "Source/IGListKit/Internal" \ -name "*.h" \ -type f -not -path "spm/*" \ -not -path "Examples/*" | sed "s| \([^/]\)|:\1|g") SRC_ROOT=$(pwd) cd $SPM_IG_LIST_KIT_SOURCES_PATH for shared_file in $shared_ig_diff_kit_sorces_list; do file_to_link=$(echo $shared_file | sed "s|:| |g") ln -s ../../../$file_to_link done for private_file in $private_headers_list; do file_to_link=$(echo $private_file | sed "s|:| |g") ln -s ../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_kit_spm_sources() { echo "Generate symbolic links for all public implementations. *.m" echo "Generated under ${SPM_IG_LIST_KIT_SOURCES_PATH}" sources_list=$( find "Source/IGListKit" \ -name "*.m" -o -name "*.mm" \ -type f -not -path "spm/*" \ -not -path "Examples/*" | sed "s| \([^/]\)|:\1|g") SRC_ROOT=$(pwd) cd $SPM_IG_LIST_KIT_SOURCES_PATH for source_file in $sources_list; do file_to_link=$(echo $source_file | sed "s|:| |g") ln -s ../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_kit() { generate_ig_list_kit_spm_public_headers generate_ig_list_kit_spm_private_headers generate_ig_list_kit_spm_sources } #### IGListSwiftKit SPM_IG_LIST_SWIFT_KIT_SOURCES_PATH="spm/Sources/IGListSwiftKit" function generate_ig_list_swift_kit_spm_sources() { echo "Generate symbolic links for all public implementations. *.swift" echo "Generated under ${SPM_IG_LIST_SWIFT_KIT_SOURCES_PATH}" sources_list=$( find "Source/IGListSwiftKit" \ -type f -name "*.swift" \ -not -path "spm/*" | sed "s| \([^/]\)|:\1|g") SRC_ROOT=$(pwd) cd $SPM_IG_LIST_SWIFT_KIT_SOURCES_PATH for source_file in $sources_list; do file_to_link=$(echo $source_file | sed "s|:| |g") ln -s ../../../$file_to_link done cd $SRC_ROOT echo " Done" echo "" } function generate_ig_list_swift_kit() { generate_ig_list_swift_kit_spm_sources } # Delete all symbolik links from `spm` folder function cleanup() { rm -rf $SPM_IG_LIST_DIFF_KIT_PUBLIC_HEADERS_PATH/*.* rm -rf $SPM_IG_LIST_DIFF_KIT_SOURCES_PATH/*.* #IGListKit rm -rf $SPM_IG_LIST_KIT_PUBLIC_HEADERS_PATH/*.* rm -rf $SPM_IG_LIST_KIT_SOURCES_PATH/*.* #IGListSwiftKit rm -rf $SPM_IG_LIST_SWIFT_KIT_SOURCES_PATH/*.* } ########## SPM generator pipeline ############# #1 cleanup #2 generate_ig_list_diff_kit #3 generate_ig_list_kit #4 generate_ig_list_swift_kit ================================================ FILE: scripts/lint.sh ================================================ #!/bin/bash # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. # Adds support for Apple Silicon brew directory if test -d "/opt/homebrew/bin/"; then PATH="/opt/homebrew/bin/:${PATH}" export PATH fi # Make sure bash executes from the same directory as the script cd "$(cd -P -- "$(dirname -- "$0")" && pwd -P)" VERSION="0.50.3" FOUND=$(swiftlint version) if which swiftlint >/dev/null; then swiftlint lint --config ../.swiftlint.yml else echo " Warning: SwiftLint not installed! You should download SwiftLint to verify your Swift code. Download from https://github.com/realm/SwiftLint, or brew install swiftlint. " exit fi if [ $(swiftlint version) != $VERSION ]; then echo " Warning: incorrect SwiftLint installed! Expected: $VERSION Found: $FOUND Download from https://github.com/realm/SwiftLint, or brew upgrade swiftlint. " fi exit ================================================ FILE: scripts/version.sh ================================================ #!/bin/bash # Copyright (c) Meta Platforms, Inc. and affiliates. # # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. cd "$(dirname "$(dirname "$0")")" || exit 1 exec /usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$(pwd)/Source/Info.plist"