├── .gitignore ├── ModernCollectionViews.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── ModernCollectionViews ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── SceneDelegate.swift ├── Scenes │ ├── CellRegistration │ │ ├── CellRegistrationCoordinator.swift │ │ ├── CellRegistrationTopicContentConfiguration.swift │ │ ├── CellRegistrationTopicViewController.swift │ │ └── CellRegistrationViewController.swift │ ├── CompositionalLayout │ │ ├── CompositionalLayoutProtocols.swift │ │ ├── CompositionalLayoutTopicCoordinator.swift │ │ ├── CompositionalLayoutTopicViewController.swift │ │ ├── CompositionalLayoutTopicViewFactory.swift │ │ └── Layouts │ │ │ ├── LayoutProtocol.swift │ │ │ ├── ListLayout.swift │ │ │ ├── NestedGroupLayout.swift │ │ │ ├── NestedGroupV2Layout.swift │ │ │ └── SquareLayout.swift │ ├── DiffableDataSource │ │ ├── BluetoothSettings │ │ │ ├── BluetoothItem.swift │ │ │ ├── BluetoothSettingsCoordinator.swift │ │ │ └── BluetoothSettingsViewController.swift │ │ ├── DiffableDataSourceCoordinator.swift │ │ ├── DiffableDataSourceViewController.swift │ │ └── OddEvenNumbers │ │ │ ├── OddEvenNumbersCoordinator.swift │ │ │ ├── OddEvenNumbersProtocols.swift │ │ │ ├── OddEvenNumbersView.swift │ │ │ ├── OddEvenNumbersViewController.swift │ │ │ ├── OddEvenNumbersViewFactory.swift │ │ │ └── OddEvenNumbersViewModel.swift │ ├── Lists │ │ ├── Extensions │ │ │ └── Appearance+Title.swift │ │ ├── ListTopicCoordinator.swift │ │ ├── ListsTopicContentAppearance.swift │ │ └── ListsTopicViewController.swift │ ├── Main │ │ ├── MainCoordinator.swift │ │ ├── MainProtocols.swift │ │ ├── MainViewController.swift │ │ ├── MainViewFactory.swift │ │ ├── MainViewSection.swift │ │ └── ViewComponents │ │ │ └── SectionHeaderView.swift │ ├── MultipleLayouts │ │ ├── MultipleLayoutsCoordinator.swift │ │ ├── MultipleLayoutsViewController.swift │ │ └── MultipleLayoutsViewFactory.swift │ └── OtherCapabilities │ │ ├── OtherCapabilitiesCoordinator.swift │ │ ├── OtherCapabilitiesItemModel.swift │ │ └── OtherCapabilitiesViewController.swift └── Shared │ ├── Extensions │ ├── Array+Filtering.swift │ └── UICollectionView+Dequeuing.swift │ ├── Protocols │ ├── Coordinator.swift │ └── Dequeuable.swift │ └── ViewComponents │ └── NumberedCollectionViewCell.swift ├── ModernCollectionViewsTests ├── Info.plist └── ModernCollectionViewsTests.swift ├── ModernCollectionViewsUITests ├── Info.plist └── ModernCollectionViewsUITests.swift ├── README.md └── Screenshots ├── Compositional.png ├── Diffable.png ├── Home.png └── Lists.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /ModernCollectionViews.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E21710312537EDB200C1CFD8 /* OddEvenNumbersProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21710302537EDB200C1CFD8 /* OddEvenNumbersProtocols.swift */; }; 11 | E21710392537F02D00C1CFD8 /* CompositionalLayoutProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21710382537F02D00C1CFD8 /* CompositionalLayoutProtocols.swift */; }; 12 | E21710412537F08000C1CFD8 /* MainProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21710402537F08000C1CFD8 /* MainProtocols.swift */; }; 13 | E217104C2537F42400C1CFD8 /* MainViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E217104B2537F42400C1CFD8 /* MainViewFactory.swift */; }; 14 | E2391EE22532D408004E49AB /* ListTopicCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2391EE12532D408004E49AB /* ListTopicCoordinator.swift */; }; 15 | E25B40CA25E2293C0030ABD9 /* ListsTopicContentAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25B40C925E2293C0030ABD9 /* ListsTopicContentAppearance.swift */; }; 16 | E25B40D025E22DAC0030ABD9 /* CellRegistrationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25B40CF25E22DAC0030ABD9 /* CellRegistrationViewController.swift */; }; 17 | E25B40D825E22DFD0030ABD9 /* CellRegistrationTopicContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25B40D725E22DFD0030ABD9 /* CellRegistrationTopicContentConfiguration.swift */; }; 18 | E268A4F225E41D43006B3A8F /* OtherCapabilitiesItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268A4F125E41D43006B3A8F /* OtherCapabilitiesItemModel.swift */; }; 19 | E268A4FB25E42B12006B3A8F /* MultipleLayoutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268A4FA25E42B12006B3A8F /* MultipleLayoutsViewController.swift */; }; 20 | E268A50325E42B1E006B3A8F /* MultipleLayoutsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268A50225E42B1E006B3A8F /* MultipleLayoutsCoordinator.swift */; }; 21 | E268A57225E44009006B3A8F /* MultipleLayoutsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268A57125E44009006B3A8F /* MultipleLayoutsViewFactory.swift */; }; 22 | E2696B252533E97D00A13F9D /* CompositionalLayoutTopicCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2696B242533E97D00A13F9D /* CompositionalLayoutTopicCoordinator.swift */; }; 23 | E2696B2A253412FA00A13F9D /* OddEvenNumbersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2696B29253412FA00A13F9D /* OddEvenNumbersCoordinator.swift */; }; 24 | E2696B2F253422B500A13F9D /* OddEvenNumbersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2696B2E253422B500A13F9D /* OddEvenNumbersViewModel.swift */; }; 25 | E26B734025E5B995000C28B8 /* DiffableDataSourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B733F25E5B995000C28B8 /* DiffableDataSourceViewController.swift */; }; 26 | E26B734525E5B9A7000C28B8 /* DiffableDataSourceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B734425E5B9A7000C28B8 /* DiffableDataSourceCoordinator.swift */; }; 27 | E26B734A25E5FFCA000C28B8 /* BluetoothSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B734925E5FFCA000C28B8 /* BluetoothSettingsViewController.swift */; }; 28 | E26B734F25E60003000C28B8 /* BluetoothSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B734E25E60003000C28B8 /* BluetoothSettingsCoordinator.swift */; }; 29 | E26B735525E60D1C000C28B8 /* BluetoothItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B735425E60D1C000C28B8 /* BluetoothItem.swift */; }; 30 | E26B735B25E60EFA000C28B8 /* SquareLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B735A25E60EFA000C28B8 /* SquareLayout.swift */; }; 31 | E26B736325E60F70000C28B8 /* LayoutProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B736225E60F70000C28B8 /* LayoutProtocol.swift */; }; 32 | E26B736825E60FD2000C28B8 /* ListLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B736725E60FD2000C28B8 /* ListLayout.swift */; }; 33 | E26B736D25E60FE4000C28B8 /* NestedGroupLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B736C25E60FE4000C28B8 /* NestedGroupLayout.swift */; }; 34 | E26B737225E61046000C28B8 /* NestedGroupV2Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26B737125E61046000C28B8 /* NestedGroupV2Layout.swift */; }; 35 | E272304825300AAC004D876A /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E272304725300AAC004D876A /* Coordinator.swift */; }; 36 | E272304D253010B9004D876A /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E272304C253010B9004D876A /* MainCoordinator.swift */; }; 37 | E2796CCB25E31A95006AAF24 /* Appearance+Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2796CCA25E31A95006AAF24 /* Appearance+Title.swift */; }; 38 | E2796CD025E31C54006AAF24 /* CellRegistrationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2796CCF25E31C54006AAF24 /* CellRegistrationCoordinator.swift */; }; 39 | E2796CD925E33943006AAF24 /* OtherCapabilitiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2796CD825E33943006AAF24 /* OtherCapabilitiesViewController.swift */; }; 40 | E2796CE125E33950006AAF24 /* OtherCapabilitiesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2796CE025E33950006AAF24 /* OtherCapabilitiesCoordinator.swift */; }; 41 | E2B5F9CC252D7BE60018293C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5F9CB252D7BE60018293C /* AppDelegate.swift */; }; 42 | E2B5F9CE252D7BE60018293C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5F9CD252D7BE60018293C /* SceneDelegate.swift */; }; 43 | E2B5F9D5252D7BE90018293C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2B5F9D4252D7BE90018293C /* Assets.xcassets */; }; 44 | E2B5F9D8252D7BE90018293C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2B5F9D6252D7BE90018293C /* LaunchScreen.storyboard */; }; 45 | E2B5F9E3252D7BEA0018293C /* ModernCollectionViewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5F9E2252D7BEA0018293C /* ModernCollectionViewsTests.swift */; }; 46 | E2B5F9EE252D7BEA0018293C /* ModernCollectionViewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5F9ED252D7BEA0018293C /* ModernCollectionViewsUITests.swift */; }; 47 | E2B5FA11252D7C5B0018293C /* MainViewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA0D252D7C5A0018293C /* MainViewSection.swift */; }; 48 | E2B5FA13252D7C5B0018293C /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA0F252D7C5B0018293C /* MainViewController.swift */; }; 49 | E2B5FA1A252D7C720018293C /* SectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA19252D7C720018293C /* SectionHeaderView.swift */; }; 50 | E2B5FA27252D7C7F0018293C /* OddEvenNumbersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA1F252D7C7F0018293C /* OddEvenNumbersViewController.swift */; }; 51 | E2B5FA28252D7C7F0018293C /* OddEvenNumbersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA20252D7C7F0018293C /* OddEvenNumbersView.swift */; }; 52 | E2B5FA29252D7C7F0018293C /* OddEvenNumbersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA21252D7C7F0018293C /* OddEvenNumbersViewFactory.swift */; }; 53 | E2B5FA2A252D7C7F0018293C /* CompositionalLayoutTopicViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA23252D7C7F0018293C /* CompositionalLayoutTopicViewFactory.swift */; }; 54 | E2B5FA2B252D7C7F0018293C /* CompositionalLayoutTopicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA24252D7C7F0018293C /* CompositionalLayoutTopicViewController.swift */; }; 55 | E2B5FA2C252D7C7F0018293C /* ListsTopicViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA26252D7C7F0018293C /* ListsTopicViewController.swift */; }; 56 | E2B5FA37252D7C930018293C /* Dequeuable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA31252D7C930018293C /* Dequeuable.swift */; }; 57 | E2B5FA38252D7C930018293C /* UICollectionView+Dequeuing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA33252D7C930018293C /* UICollectionView+Dequeuing.swift */; }; 58 | E2B5FA39252D7C930018293C /* Array+Filtering.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA34252D7C930018293C /* Array+Filtering.swift */; }; 59 | E2B5FA3A252D7C930018293C /* NumberedCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5FA36252D7C930018293C /* NumberedCollectionViewCell.swift */; }; 60 | /* End PBXBuildFile section */ 61 | 62 | /* Begin PBXContainerItemProxy section */ 63 | E2B5F9DF252D7BEA0018293C /* PBXContainerItemProxy */ = { 64 | isa = PBXContainerItemProxy; 65 | containerPortal = E2B5F9C0252D7BE60018293C /* Project object */; 66 | proxyType = 1; 67 | remoteGlobalIDString = E2B5F9C7252D7BE60018293C; 68 | remoteInfo = ModernCollectionViews; 69 | }; 70 | E2B5F9EA252D7BEA0018293C /* PBXContainerItemProxy */ = { 71 | isa = PBXContainerItemProxy; 72 | containerPortal = E2B5F9C0252D7BE60018293C /* Project object */; 73 | proxyType = 1; 74 | remoteGlobalIDString = E2B5F9C7252D7BE60018293C; 75 | remoteInfo = ModernCollectionViews; 76 | }; 77 | /* End PBXContainerItemProxy section */ 78 | 79 | /* Begin PBXFileReference section */ 80 | E21710302537EDB200C1CFD8 /* OddEvenNumbersProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersProtocols.swift; sourceTree = ""; }; 81 | E21710382537F02D00C1CFD8 /* CompositionalLayoutProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutProtocols.swift; sourceTree = ""; }; 82 | E21710402537F08000C1CFD8 /* MainProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainProtocols.swift; sourceTree = ""; }; 83 | E217104B2537F42400C1CFD8 /* MainViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewFactory.swift; sourceTree = ""; }; 84 | E2391EE12532D408004E49AB /* ListTopicCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTopicCoordinator.swift; sourceTree = ""; }; 85 | E25B40C925E2293C0030ABD9 /* ListsTopicContentAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsTopicContentAppearance.swift; sourceTree = ""; }; 86 | E25B40CF25E22DAC0030ABD9 /* CellRegistrationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellRegistrationViewController.swift; sourceTree = ""; }; 87 | E25B40D725E22DFD0030ABD9 /* CellRegistrationTopicContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellRegistrationTopicContentConfiguration.swift; sourceTree = ""; }; 88 | E268A4F125E41D43006B3A8F /* OtherCapabilitiesItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCapabilitiesItemModel.swift; sourceTree = ""; }; 89 | E268A4FA25E42B12006B3A8F /* MultipleLayoutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLayoutsViewController.swift; sourceTree = ""; }; 90 | E268A50225E42B1E006B3A8F /* MultipleLayoutsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLayoutsCoordinator.swift; sourceTree = ""; }; 91 | E268A57125E44009006B3A8F /* MultipleLayoutsViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLayoutsViewFactory.swift; sourceTree = ""; }; 92 | E2696B242533E97D00A13F9D /* CompositionalLayoutTopicCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutTopicCoordinator.swift; sourceTree = ""; }; 93 | E2696B29253412FA00A13F9D /* OddEvenNumbersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersCoordinator.swift; sourceTree = ""; }; 94 | E2696B2E253422B500A13F9D /* OddEvenNumbersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersViewModel.swift; sourceTree = ""; }; 95 | E26B733F25E5B995000C28B8 /* DiffableDataSourceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableDataSourceViewController.swift; sourceTree = ""; }; 96 | E26B734425E5B9A7000C28B8 /* DiffableDataSourceCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffableDataSourceCoordinator.swift; sourceTree = ""; }; 97 | E26B734925E5FFCA000C28B8 /* BluetoothSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothSettingsViewController.swift; sourceTree = ""; }; 98 | E26B734E25E60003000C28B8 /* BluetoothSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothSettingsCoordinator.swift; sourceTree = ""; }; 99 | E26B735425E60D1C000C28B8 /* BluetoothItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothItem.swift; sourceTree = ""; }; 100 | E26B735A25E60EFA000C28B8 /* SquareLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareLayout.swift; sourceTree = ""; }; 101 | E26B736225E60F70000C28B8 /* LayoutProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutProtocol.swift; sourceTree = ""; }; 102 | E26B736725E60FD2000C28B8 /* ListLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListLayout.swift; sourceTree = ""; }; 103 | E26B736C25E60FE4000C28B8 /* NestedGroupLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedGroupLayout.swift; sourceTree = ""; }; 104 | E26B737125E61046000C28B8 /* NestedGroupV2Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedGroupV2Layout.swift; sourceTree = ""; }; 105 | E272304725300AAC004D876A /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 106 | E272304C253010B9004D876A /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; }; 107 | E2796CCA25E31A95006AAF24 /* Appearance+Title.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Appearance+Title.swift"; sourceTree = ""; }; 108 | E2796CCF25E31C54006AAF24 /* CellRegistrationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellRegistrationCoordinator.swift; sourceTree = ""; }; 109 | E2796CD825E33943006AAF24 /* OtherCapabilitiesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCapabilitiesViewController.swift; sourceTree = ""; }; 110 | E2796CE025E33950006AAF24 /* OtherCapabilitiesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCapabilitiesCoordinator.swift; sourceTree = ""; }; 111 | E2B5F9C8252D7BE60018293C /* ModernCollectionViews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ModernCollectionViews.app; sourceTree = BUILT_PRODUCTS_DIR; }; 112 | E2B5F9CB252D7BE60018293C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 113 | E2B5F9CD252D7BE60018293C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 114 | E2B5F9D4252D7BE90018293C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 115 | E2B5F9D7252D7BE90018293C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 116 | E2B5F9D9252D7BE90018293C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 117 | E2B5F9DE252D7BEA0018293C /* ModernCollectionViewsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ModernCollectionViewsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 118 | E2B5F9E2252D7BEA0018293C /* ModernCollectionViewsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernCollectionViewsTests.swift; sourceTree = ""; }; 119 | E2B5F9E4252D7BEA0018293C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 120 | E2B5F9E9252D7BEA0018293C /* ModernCollectionViewsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ModernCollectionViewsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 121 | E2B5F9ED252D7BEA0018293C /* ModernCollectionViewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernCollectionViewsUITests.swift; sourceTree = ""; }; 122 | E2B5F9EF252D7BEA0018293C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | E2B5FA0D252D7C5A0018293C /* MainViewSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewSection.swift; sourceTree = ""; }; 124 | E2B5FA0F252D7C5B0018293C /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 125 | E2B5FA19252D7C720018293C /* SectionHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionHeaderView.swift; sourceTree = ""; }; 126 | E2B5FA1F252D7C7F0018293C /* OddEvenNumbersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersViewController.swift; sourceTree = ""; }; 127 | E2B5FA20252D7C7F0018293C /* OddEvenNumbersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersView.swift; sourceTree = ""; }; 128 | E2B5FA21252D7C7F0018293C /* OddEvenNumbersViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OddEvenNumbersViewFactory.swift; sourceTree = ""; }; 129 | E2B5FA23252D7C7F0018293C /* CompositionalLayoutTopicViewFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutTopicViewFactory.swift; sourceTree = ""; }; 130 | E2B5FA24252D7C7F0018293C /* CompositionalLayoutTopicViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutTopicViewController.swift; sourceTree = ""; }; 131 | E2B5FA26252D7C7F0018293C /* ListsTopicViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListsTopicViewController.swift; sourceTree = ""; }; 132 | E2B5FA31252D7C930018293C /* Dequeuable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dequeuable.swift; sourceTree = ""; }; 133 | E2B5FA33252D7C930018293C /* UICollectionView+Dequeuing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Dequeuing.swift"; sourceTree = ""; }; 134 | E2B5FA34252D7C930018293C /* Array+Filtering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Filtering.swift"; sourceTree = ""; }; 135 | E2B5FA36252D7C930018293C /* NumberedCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberedCollectionViewCell.swift; sourceTree = ""; }; 136 | /* End PBXFileReference section */ 137 | 138 | /* Begin PBXFrameworksBuildPhase section */ 139 | E2B5F9C5252D7BE60018293C /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | E2B5F9DB252D7BEA0018293C /* Frameworks */ = { 147 | isa = PBXFrameworksBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | E2B5F9E6252D7BEA0018293C /* Frameworks */ = { 154 | isa = PBXFrameworksBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXFrameworksBuildPhase section */ 161 | 162 | /* Begin PBXGroup section */ 163 | E25B40CE25E22D910030ABD9 /* CellRegistration */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | E25B40CF25E22DAC0030ABD9 /* CellRegistrationViewController.swift */, 167 | E2796CCF25E31C54006AAF24 /* CellRegistrationCoordinator.swift */, 168 | E25B40D725E22DFD0030ABD9 /* CellRegistrationTopicContentConfiguration.swift */, 169 | ); 170 | path = CellRegistration; 171 | sourceTree = ""; 172 | }; 173 | E268A4F925E42AF2006B3A8F /* MultipleLayouts */ = { 174 | isa = PBXGroup; 175 | children = ( 176 | E268A4FA25E42B12006B3A8F /* MultipleLayoutsViewController.swift */, 177 | E268A50225E42B1E006B3A8F /* MultipleLayoutsCoordinator.swift */, 178 | E268A57125E44009006B3A8F /* MultipleLayoutsViewFactory.swift */, 179 | ); 180 | path = MultipleLayouts; 181 | sourceTree = ""; 182 | }; 183 | E26B732E25E5B71E000C28B8 /* OddEvenNumbers */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | E21710302537EDB200C1CFD8 /* OddEvenNumbersProtocols.swift */, 187 | E2B5FA1F252D7C7F0018293C /* OddEvenNumbersViewController.swift */, 188 | E2B5FA20252D7C7F0018293C /* OddEvenNumbersView.swift */, 189 | E2B5FA21252D7C7F0018293C /* OddEvenNumbersViewFactory.swift */, 190 | E2696B29253412FA00A13F9D /* OddEvenNumbersCoordinator.swift */, 191 | E2696B2E253422B500A13F9D /* OddEvenNumbersViewModel.swift */, 192 | ); 193 | path = OddEvenNumbers; 194 | sourceTree = ""; 195 | }; 196 | E26B732F25E5B733000C28B8 /* BluetoothSettings */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | E26B734925E5FFCA000C28B8 /* BluetoothSettingsViewController.swift */, 200 | E26B734E25E60003000C28B8 /* BluetoothSettingsCoordinator.swift */, 201 | E26B735425E60D1C000C28B8 /* BluetoothItem.swift */, 202 | ); 203 | path = BluetoothSettings; 204 | sourceTree = ""; 205 | }; 206 | E26B735925E60ED8000C28B8 /* Layouts */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | E26B736225E60F70000C28B8 /* LayoutProtocol.swift */, 210 | E26B735A25E60EFA000C28B8 /* SquareLayout.swift */, 211 | E26B736C25E60FE4000C28B8 /* NestedGroupLayout.swift */, 212 | E26B737125E61046000C28B8 /* NestedGroupV2Layout.swift */, 213 | E26B736725E60FD2000C28B8 /* ListLayout.swift */, 214 | ); 215 | path = Layouts; 216 | sourceTree = ""; 217 | }; 218 | E2796CC925E31A84006AAF24 /* Extensions */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | E2796CCA25E31A95006AAF24 /* Appearance+Title.swift */, 222 | ); 223 | path = Extensions; 224 | sourceTree = ""; 225 | }; 226 | E2796CD725E33875006AAF24 /* OtherCapabilities */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | E2796CD825E33943006AAF24 /* OtherCapabilitiesViewController.swift */, 230 | E2796CE025E33950006AAF24 /* OtherCapabilitiesCoordinator.swift */, 231 | E268A4F125E41D43006B3A8F /* OtherCapabilitiesItemModel.swift */, 232 | ); 233 | path = OtherCapabilities; 234 | sourceTree = ""; 235 | }; 236 | E2B5F9BF252D7BE60018293C = { 237 | isa = PBXGroup; 238 | children = ( 239 | E2B5F9CA252D7BE60018293C /* ModernCollectionViews */, 240 | E2B5F9E1252D7BEA0018293C /* ModernCollectionViewsTests */, 241 | E2B5F9EC252D7BEA0018293C /* ModernCollectionViewsUITests */, 242 | E2B5F9C9252D7BE60018293C /* Products */, 243 | ); 244 | sourceTree = ""; 245 | }; 246 | E2B5F9C9252D7BE60018293C /* Products */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | E2B5F9C8252D7BE60018293C /* ModernCollectionViews.app */, 250 | E2B5F9DE252D7BEA0018293C /* ModernCollectionViewsTests.xctest */, 251 | E2B5F9E9252D7BEA0018293C /* ModernCollectionViewsUITests.xctest */, 252 | ); 253 | name = Products; 254 | sourceTree = ""; 255 | }; 256 | E2B5F9CA252D7BE60018293C /* ModernCollectionViews */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | E2B5FA02252D7C080018293C /* Scenes */, 260 | E2B5FA01252D7C010018293C /* Shared */, 261 | E2B5F9CB252D7BE60018293C /* AppDelegate.swift */, 262 | E2B5F9CD252D7BE60018293C /* SceneDelegate.swift */, 263 | E2B5F9D4252D7BE90018293C /* Assets.xcassets */, 264 | E2B5F9D6252D7BE90018293C /* LaunchScreen.storyboard */, 265 | E2B5F9D9252D7BE90018293C /* Info.plist */, 266 | ); 267 | path = ModernCollectionViews; 268 | sourceTree = ""; 269 | }; 270 | E2B5F9E1252D7BEA0018293C /* ModernCollectionViewsTests */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | E2B5F9E2252D7BEA0018293C /* ModernCollectionViewsTests.swift */, 274 | E2B5F9E4252D7BEA0018293C /* Info.plist */, 275 | ); 276 | path = ModernCollectionViewsTests; 277 | sourceTree = ""; 278 | }; 279 | E2B5F9EC252D7BEA0018293C /* ModernCollectionViewsUITests */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | E2B5F9ED252D7BEA0018293C /* ModernCollectionViewsUITests.swift */, 283 | E2B5F9EF252D7BEA0018293C /* Info.plist */, 284 | ); 285 | path = ModernCollectionViewsUITests; 286 | sourceTree = ""; 287 | }; 288 | E2B5FA01252D7C010018293C /* Shared */ = { 289 | isa = PBXGroup; 290 | children = ( 291 | E2B5FA32252D7C930018293C /* Extensions */, 292 | E2B5FA30252D7C930018293C /* Protocols */, 293 | E2B5FA35252D7C930018293C /* ViewComponents */, 294 | ); 295 | path = Shared; 296 | sourceTree = ""; 297 | }; 298 | E2B5FA02252D7C080018293C /* Scenes */ = { 299 | isa = PBXGroup; 300 | children = ( 301 | E2B5FA0C252D7C4E0018293C /* Main */, 302 | E2B5FA22252D7C7F0018293C /* CompositionalLayout */, 303 | E2B5FA1E252D7C7F0018293C /* DiffableDataSource */, 304 | E2B5FA25252D7C7F0018293C /* Lists */, 305 | E25B40CE25E22D910030ABD9 /* CellRegistration */, 306 | E268A4F925E42AF2006B3A8F /* MultipleLayouts */, 307 | E2796CD725E33875006AAF24 /* OtherCapabilities */, 308 | ); 309 | path = Scenes; 310 | sourceTree = ""; 311 | }; 312 | E2B5FA0C252D7C4E0018293C /* Main */ = { 313 | isa = PBXGroup; 314 | children = ( 315 | E2B5FA18252D7C610018293C /* ViewComponents */, 316 | E21710402537F08000C1CFD8 /* MainProtocols.swift */, 317 | E2B5FA0F252D7C5B0018293C /* MainViewController.swift */, 318 | E2B5FA0D252D7C5A0018293C /* MainViewSection.swift */, 319 | E272304C253010B9004D876A /* MainCoordinator.swift */, 320 | E217104B2537F42400C1CFD8 /* MainViewFactory.swift */, 321 | ); 322 | path = Main; 323 | sourceTree = ""; 324 | }; 325 | E2B5FA18252D7C610018293C /* ViewComponents */ = { 326 | isa = PBXGroup; 327 | children = ( 328 | E2B5FA19252D7C720018293C /* SectionHeaderView.swift */, 329 | ); 330 | path = ViewComponents; 331 | sourceTree = ""; 332 | }; 333 | E2B5FA1E252D7C7F0018293C /* DiffableDataSource */ = { 334 | isa = PBXGroup; 335 | children = ( 336 | E26B732F25E5B733000C28B8 /* BluetoothSettings */, 337 | E26B732E25E5B71E000C28B8 /* OddEvenNumbers */, 338 | E26B733F25E5B995000C28B8 /* DiffableDataSourceViewController.swift */, 339 | E26B734425E5B9A7000C28B8 /* DiffableDataSourceCoordinator.swift */, 340 | ); 341 | path = DiffableDataSource; 342 | sourceTree = ""; 343 | }; 344 | E2B5FA22252D7C7F0018293C /* CompositionalLayout */ = { 345 | isa = PBXGroup; 346 | children = ( 347 | E26B735925E60ED8000C28B8 /* Layouts */, 348 | E21710382537F02D00C1CFD8 /* CompositionalLayoutProtocols.swift */, 349 | E2B5FA23252D7C7F0018293C /* CompositionalLayoutTopicViewFactory.swift */, 350 | E2B5FA24252D7C7F0018293C /* CompositionalLayoutTopicViewController.swift */, 351 | E2696B242533E97D00A13F9D /* CompositionalLayoutTopicCoordinator.swift */, 352 | ); 353 | path = CompositionalLayout; 354 | sourceTree = ""; 355 | }; 356 | E2B5FA25252D7C7F0018293C /* Lists */ = { 357 | isa = PBXGroup; 358 | children = ( 359 | E2796CC925E31A84006AAF24 /* Extensions */, 360 | E2B5FA26252D7C7F0018293C /* ListsTopicViewController.swift */, 361 | E2391EE12532D408004E49AB /* ListTopicCoordinator.swift */, 362 | E25B40C925E2293C0030ABD9 /* ListsTopicContentAppearance.swift */, 363 | ); 364 | path = Lists; 365 | sourceTree = ""; 366 | }; 367 | E2B5FA30252D7C930018293C /* Protocols */ = { 368 | isa = PBXGroup; 369 | children = ( 370 | E2B5FA31252D7C930018293C /* Dequeuable.swift */, 371 | E272304725300AAC004D876A /* Coordinator.swift */, 372 | ); 373 | path = Protocols; 374 | sourceTree = ""; 375 | }; 376 | E2B5FA32252D7C930018293C /* Extensions */ = { 377 | isa = PBXGroup; 378 | children = ( 379 | E2B5FA33252D7C930018293C /* UICollectionView+Dequeuing.swift */, 380 | E2B5FA34252D7C930018293C /* Array+Filtering.swift */, 381 | ); 382 | path = Extensions; 383 | sourceTree = ""; 384 | }; 385 | E2B5FA35252D7C930018293C /* ViewComponents */ = { 386 | isa = PBXGroup; 387 | children = ( 388 | E2B5FA36252D7C930018293C /* NumberedCollectionViewCell.swift */, 389 | ); 390 | path = ViewComponents; 391 | sourceTree = ""; 392 | }; 393 | /* End PBXGroup section */ 394 | 395 | /* Begin PBXNativeTarget section */ 396 | E2B5F9C7252D7BE60018293C /* ModernCollectionViews */ = { 397 | isa = PBXNativeTarget; 398 | buildConfigurationList = E2B5F9F2252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViews" */; 399 | buildPhases = ( 400 | E2B5F9C4252D7BE60018293C /* Sources */, 401 | E2B5F9C5252D7BE60018293C /* Frameworks */, 402 | E2B5F9C6252D7BE60018293C /* Resources */, 403 | ); 404 | buildRules = ( 405 | ); 406 | dependencies = ( 407 | ); 408 | name = ModernCollectionViews; 409 | productName = ModernCollectionViews; 410 | productReference = E2B5F9C8252D7BE60018293C /* ModernCollectionViews.app */; 411 | productType = "com.apple.product-type.application"; 412 | }; 413 | E2B5F9DD252D7BEA0018293C /* ModernCollectionViewsTests */ = { 414 | isa = PBXNativeTarget; 415 | buildConfigurationList = E2B5F9F5252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViewsTests" */; 416 | buildPhases = ( 417 | E2B5F9DA252D7BEA0018293C /* Sources */, 418 | E2B5F9DB252D7BEA0018293C /* Frameworks */, 419 | E2B5F9DC252D7BEA0018293C /* Resources */, 420 | ); 421 | buildRules = ( 422 | ); 423 | dependencies = ( 424 | E2B5F9E0252D7BEA0018293C /* PBXTargetDependency */, 425 | ); 426 | name = ModernCollectionViewsTests; 427 | productName = ModernCollectionViewsTests; 428 | productReference = E2B5F9DE252D7BEA0018293C /* ModernCollectionViewsTests.xctest */; 429 | productType = "com.apple.product-type.bundle.unit-test"; 430 | }; 431 | E2B5F9E8252D7BEA0018293C /* ModernCollectionViewsUITests */ = { 432 | isa = PBXNativeTarget; 433 | buildConfigurationList = E2B5F9F8252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViewsUITests" */; 434 | buildPhases = ( 435 | E2B5F9E5252D7BEA0018293C /* Sources */, 436 | E2B5F9E6252D7BEA0018293C /* Frameworks */, 437 | E2B5F9E7252D7BEA0018293C /* Resources */, 438 | ); 439 | buildRules = ( 440 | ); 441 | dependencies = ( 442 | E2B5F9EB252D7BEA0018293C /* PBXTargetDependency */, 443 | ); 444 | name = ModernCollectionViewsUITests; 445 | productName = ModernCollectionViewsUITests; 446 | productReference = E2B5F9E9252D7BEA0018293C /* ModernCollectionViewsUITests.xctest */; 447 | productType = "com.apple.product-type.bundle.ui-testing"; 448 | }; 449 | /* End PBXNativeTarget section */ 450 | 451 | /* Begin PBXProject section */ 452 | E2B5F9C0252D7BE60018293C /* Project object */ = { 453 | isa = PBXProject; 454 | attributes = { 455 | LastSwiftUpdateCheck = 1200; 456 | LastUpgradeCheck = 1200; 457 | TargetAttributes = { 458 | E2B5F9C7252D7BE60018293C = { 459 | CreatedOnToolsVersion = 12.0.1; 460 | }; 461 | E2B5F9DD252D7BEA0018293C = { 462 | CreatedOnToolsVersion = 12.0.1; 463 | TestTargetID = E2B5F9C7252D7BE60018293C; 464 | }; 465 | E2B5F9E8252D7BEA0018293C = { 466 | CreatedOnToolsVersion = 12.0.1; 467 | TestTargetID = E2B5F9C7252D7BE60018293C; 468 | }; 469 | }; 470 | }; 471 | buildConfigurationList = E2B5F9C3252D7BE60018293C /* Build configuration list for PBXProject "ModernCollectionViews" */; 472 | compatibilityVersion = "Xcode 9.3"; 473 | developmentRegion = en; 474 | hasScannedForEncodings = 0; 475 | knownRegions = ( 476 | en, 477 | Base, 478 | ); 479 | mainGroup = E2B5F9BF252D7BE60018293C; 480 | productRefGroup = E2B5F9C9252D7BE60018293C /* Products */; 481 | projectDirPath = ""; 482 | projectRoot = ""; 483 | targets = ( 484 | E2B5F9C7252D7BE60018293C /* ModernCollectionViews */, 485 | E2B5F9DD252D7BEA0018293C /* ModernCollectionViewsTests */, 486 | E2B5F9E8252D7BEA0018293C /* ModernCollectionViewsUITests */, 487 | ); 488 | }; 489 | /* End PBXProject section */ 490 | 491 | /* Begin PBXResourcesBuildPhase section */ 492 | E2B5F9C6252D7BE60018293C /* Resources */ = { 493 | isa = PBXResourcesBuildPhase; 494 | buildActionMask = 2147483647; 495 | files = ( 496 | E2B5F9D8252D7BE90018293C /* LaunchScreen.storyboard in Resources */, 497 | E2B5F9D5252D7BE90018293C /* Assets.xcassets in Resources */, 498 | ); 499 | runOnlyForDeploymentPostprocessing = 0; 500 | }; 501 | E2B5F9DC252D7BEA0018293C /* Resources */ = { 502 | isa = PBXResourcesBuildPhase; 503 | buildActionMask = 2147483647; 504 | files = ( 505 | ); 506 | runOnlyForDeploymentPostprocessing = 0; 507 | }; 508 | E2B5F9E7252D7BEA0018293C /* Resources */ = { 509 | isa = PBXResourcesBuildPhase; 510 | buildActionMask = 2147483647; 511 | files = ( 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | /* End PBXResourcesBuildPhase section */ 516 | 517 | /* Begin PBXSourcesBuildPhase section */ 518 | E2B5F9C4252D7BE60018293C /* Sources */ = { 519 | isa = PBXSourcesBuildPhase; 520 | buildActionMask = 2147483647; 521 | files = ( 522 | E26B734525E5B9A7000C28B8 /* DiffableDataSourceCoordinator.swift in Sources */, 523 | E2696B2A253412FA00A13F9D /* OddEvenNumbersCoordinator.swift in Sources */, 524 | E2796CE125E33950006AAF24 /* OtherCapabilitiesCoordinator.swift in Sources */, 525 | E26B736D25E60FE4000C28B8 /* NestedGroupLayout.swift in Sources */, 526 | E26B734A25E5FFCA000C28B8 /* BluetoothSettingsViewController.swift in Sources */, 527 | E2B5FA2B252D7C7F0018293C /* CompositionalLayoutTopicViewController.swift in Sources */, 528 | E2B5FA11252D7C5B0018293C /* MainViewSection.swift in Sources */, 529 | E26B734025E5B995000C28B8 /* DiffableDataSourceViewController.swift in Sources */, 530 | E217104C2537F42400C1CFD8 /* MainViewFactory.swift in Sources */, 531 | E2B5F9CC252D7BE60018293C /* AppDelegate.swift in Sources */, 532 | E2B5F9CE252D7BE60018293C /* SceneDelegate.swift in Sources */, 533 | E2B5FA27252D7C7F0018293C /* OddEvenNumbersViewController.swift in Sources */, 534 | E25B40D825E22DFD0030ABD9 /* CellRegistrationTopicContentConfiguration.swift in Sources */, 535 | E2B5FA38252D7C930018293C /* UICollectionView+Dequeuing.swift in Sources */, 536 | E268A4FB25E42B12006B3A8F /* MultipleLayoutsViewController.swift in Sources */, 537 | E2796CD925E33943006AAF24 /* OtherCapabilitiesViewController.swift in Sources */, 538 | E268A57225E44009006B3A8F /* MultipleLayoutsViewFactory.swift in Sources */, 539 | E2796CD025E31C54006AAF24 /* CellRegistrationCoordinator.swift in Sources */, 540 | E25B40CA25E2293C0030ABD9 /* ListsTopicContentAppearance.swift in Sources */, 541 | E2B5FA29252D7C7F0018293C /* OddEvenNumbersViewFactory.swift in Sources */, 542 | E272304825300AAC004D876A /* Coordinator.swift in Sources */, 543 | E2B5FA28252D7C7F0018293C /* OddEvenNumbersView.swift in Sources */, 544 | E26B735525E60D1C000C28B8 /* BluetoothItem.swift in Sources */, 545 | E21710392537F02D00C1CFD8 /* CompositionalLayoutProtocols.swift in Sources */, 546 | E2391EE22532D408004E49AB /* ListTopicCoordinator.swift in Sources */, 547 | E2B5FA2C252D7C7F0018293C /* ListsTopicViewController.swift in Sources */, 548 | E2B5FA1A252D7C720018293C /* SectionHeaderView.swift in Sources */, 549 | E2B5FA37252D7C930018293C /* Dequeuable.swift in Sources */, 550 | E2796CCB25E31A95006AAF24 /* Appearance+Title.swift in Sources */, 551 | E21710412537F08000C1CFD8 /* MainProtocols.swift in Sources */, 552 | E268A4F225E41D43006B3A8F /* OtherCapabilitiesItemModel.swift in Sources */, 553 | E26B736325E60F70000C28B8 /* LayoutProtocol.swift in Sources */, 554 | E26B735B25E60EFA000C28B8 /* SquareLayout.swift in Sources */, 555 | E2B5FA13252D7C5B0018293C /* MainViewController.swift in Sources */, 556 | E25B40D025E22DAC0030ABD9 /* CellRegistrationViewController.swift in Sources */, 557 | E272304D253010B9004D876A /* MainCoordinator.swift in Sources */, 558 | E26B734F25E60003000C28B8 /* BluetoothSettingsCoordinator.swift in Sources */, 559 | E2696B2F253422B500A13F9D /* OddEvenNumbersViewModel.swift in Sources */, 560 | E26B737225E61046000C28B8 /* NestedGroupV2Layout.swift in Sources */, 561 | E2B5FA2A252D7C7F0018293C /* CompositionalLayoutTopicViewFactory.swift in Sources */, 562 | E21710312537EDB200C1CFD8 /* OddEvenNumbersProtocols.swift in Sources */, 563 | E268A50325E42B1E006B3A8F /* MultipleLayoutsCoordinator.swift in Sources */, 564 | E2B5FA39252D7C930018293C /* Array+Filtering.swift in Sources */, 565 | E26B736825E60FD2000C28B8 /* ListLayout.swift in Sources */, 566 | E2B5FA3A252D7C930018293C /* NumberedCollectionViewCell.swift in Sources */, 567 | E2696B252533E97D00A13F9D /* CompositionalLayoutTopicCoordinator.swift in Sources */, 568 | ); 569 | runOnlyForDeploymentPostprocessing = 0; 570 | }; 571 | E2B5F9DA252D7BEA0018293C /* Sources */ = { 572 | isa = PBXSourcesBuildPhase; 573 | buildActionMask = 2147483647; 574 | files = ( 575 | E2B5F9E3252D7BEA0018293C /* ModernCollectionViewsTests.swift in Sources */, 576 | ); 577 | runOnlyForDeploymentPostprocessing = 0; 578 | }; 579 | E2B5F9E5252D7BEA0018293C /* Sources */ = { 580 | isa = PBXSourcesBuildPhase; 581 | buildActionMask = 2147483647; 582 | files = ( 583 | E2B5F9EE252D7BEA0018293C /* ModernCollectionViewsUITests.swift in Sources */, 584 | ); 585 | runOnlyForDeploymentPostprocessing = 0; 586 | }; 587 | /* End PBXSourcesBuildPhase section */ 588 | 589 | /* Begin PBXTargetDependency section */ 590 | E2B5F9E0252D7BEA0018293C /* PBXTargetDependency */ = { 591 | isa = PBXTargetDependency; 592 | target = E2B5F9C7252D7BE60018293C /* ModernCollectionViews */; 593 | targetProxy = E2B5F9DF252D7BEA0018293C /* PBXContainerItemProxy */; 594 | }; 595 | E2B5F9EB252D7BEA0018293C /* PBXTargetDependency */ = { 596 | isa = PBXTargetDependency; 597 | target = E2B5F9C7252D7BE60018293C /* ModernCollectionViews */; 598 | targetProxy = E2B5F9EA252D7BEA0018293C /* PBXContainerItemProxy */; 599 | }; 600 | /* End PBXTargetDependency section */ 601 | 602 | /* Begin PBXVariantGroup section */ 603 | E2B5F9D6252D7BE90018293C /* LaunchScreen.storyboard */ = { 604 | isa = PBXVariantGroup; 605 | children = ( 606 | E2B5F9D7252D7BE90018293C /* Base */, 607 | ); 608 | name = LaunchScreen.storyboard; 609 | sourceTree = ""; 610 | }; 611 | /* End PBXVariantGroup section */ 612 | 613 | /* Begin XCBuildConfiguration section */ 614 | E2B5F9F0252D7BEA0018293C /* Debug */ = { 615 | isa = XCBuildConfiguration; 616 | buildSettings = { 617 | ALWAYS_SEARCH_USER_PATHS = NO; 618 | CLANG_ANALYZER_NONNULL = YES; 619 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 620 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 621 | CLANG_CXX_LIBRARY = "libc++"; 622 | CLANG_ENABLE_MODULES = YES; 623 | CLANG_ENABLE_OBJC_ARC = YES; 624 | CLANG_ENABLE_OBJC_WEAK = YES; 625 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 626 | CLANG_WARN_BOOL_CONVERSION = YES; 627 | CLANG_WARN_COMMA = YES; 628 | CLANG_WARN_CONSTANT_CONVERSION = YES; 629 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 630 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 631 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 632 | CLANG_WARN_EMPTY_BODY = YES; 633 | CLANG_WARN_ENUM_CONVERSION = YES; 634 | CLANG_WARN_INFINITE_RECURSION = YES; 635 | CLANG_WARN_INT_CONVERSION = YES; 636 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 637 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 638 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 639 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 640 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 641 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 642 | CLANG_WARN_STRICT_PROTOTYPES = YES; 643 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 644 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 645 | CLANG_WARN_UNREACHABLE_CODE = YES; 646 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 647 | COPY_PHASE_STRIP = NO; 648 | DEBUG_INFORMATION_FORMAT = dwarf; 649 | ENABLE_STRICT_OBJC_MSGSEND = YES; 650 | ENABLE_TESTABILITY = YES; 651 | GCC_C_LANGUAGE_STANDARD = gnu11; 652 | GCC_DYNAMIC_NO_PIC = NO; 653 | GCC_NO_COMMON_BLOCKS = YES; 654 | GCC_OPTIMIZATION_LEVEL = 0; 655 | GCC_PREPROCESSOR_DEFINITIONS = ( 656 | "DEBUG=1", 657 | "$(inherited)", 658 | ); 659 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 660 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 661 | GCC_WARN_UNDECLARED_SELECTOR = YES; 662 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 663 | GCC_WARN_UNUSED_FUNCTION = YES; 664 | GCC_WARN_UNUSED_VARIABLE = YES; 665 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 666 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 667 | MTL_FAST_MATH = YES; 668 | ONLY_ACTIVE_ARCH = YES; 669 | SDKROOT = iphoneos; 670 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 671 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 672 | }; 673 | name = Debug; 674 | }; 675 | E2B5F9F1252D7BEA0018293C /* Release */ = { 676 | isa = XCBuildConfiguration; 677 | buildSettings = { 678 | ALWAYS_SEARCH_USER_PATHS = NO; 679 | CLANG_ANALYZER_NONNULL = YES; 680 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 681 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 682 | CLANG_CXX_LIBRARY = "libc++"; 683 | CLANG_ENABLE_MODULES = YES; 684 | CLANG_ENABLE_OBJC_ARC = YES; 685 | CLANG_ENABLE_OBJC_WEAK = YES; 686 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 687 | CLANG_WARN_BOOL_CONVERSION = YES; 688 | CLANG_WARN_COMMA = YES; 689 | CLANG_WARN_CONSTANT_CONVERSION = YES; 690 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 691 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 692 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 693 | CLANG_WARN_EMPTY_BODY = YES; 694 | CLANG_WARN_ENUM_CONVERSION = YES; 695 | CLANG_WARN_INFINITE_RECURSION = YES; 696 | CLANG_WARN_INT_CONVERSION = YES; 697 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 698 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 699 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 700 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 701 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 702 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 703 | CLANG_WARN_STRICT_PROTOTYPES = YES; 704 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 705 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 706 | CLANG_WARN_UNREACHABLE_CODE = YES; 707 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 708 | COPY_PHASE_STRIP = NO; 709 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 710 | ENABLE_NS_ASSERTIONS = NO; 711 | ENABLE_STRICT_OBJC_MSGSEND = YES; 712 | GCC_C_LANGUAGE_STANDARD = gnu11; 713 | GCC_NO_COMMON_BLOCKS = YES; 714 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 715 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 716 | GCC_WARN_UNDECLARED_SELECTOR = YES; 717 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 718 | GCC_WARN_UNUSED_FUNCTION = YES; 719 | GCC_WARN_UNUSED_VARIABLE = YES; 720 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 721 | MTL_ENABLE_DEBUG_INFO = NO; 722 | MTL_FAST_MATH = YES; 723 | SDKROOT = iphoneos; 724 | SWIFT_COMPILATION_MODE = wholemodule; 725 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 726 | VALIDATE_PRODUCT = YES; 727 | }; 728 | name = Release; 729 | }; 730 | E2B5F9F3252D7BEA0018293C /* Debug */ = { 731 | isa = XCBuildConfiguration; 732 | buildSettings = { 733 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 734 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 735 | CODE_SIGN_STYLE = Automatic; 736 | DEVELOPMENT_TEAM = E95KU8J3PN; 737 | INFOPLIST_FILE = ModernCollectionViews/Info.plist; 738 | LD_RUNPATH_SEARCH_PATHS = ( 739 | "$(inherited)", 740 | "@executable_path/Frameworks", 741 | ); 742 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViews; 743 | PRODUCT_NAME = "$(TARGET_NAME)"; 744 | SWIFT_VERSION = 5.0; 745 | TARGETED_DEVICE_FAMILY = "1,2"; 746 | }; 747 | name = Debug; 748 | }; 749 | E2B5F9F4252D7BEA0018293C /* Release */ = { 750 | isa = XCBuildConfiguration; 751 | buildSettings = { 752 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 753 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 754 | CODE_SIGN_STYLE = Automatic; 755 | DEVELOPMENT_TEAM = E95KU8J3PN; 756 | INFOPLIST_FILE = ModernCollectionViews/Info.plist; 757 | LD_RUNPATH_SEARCH_PATHS = ( 758 | "$(inherited)", 759 | "@executable_path/Frameworks", 760 | ); 761 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViews; 762 | PRODUCT_NAME = "$(TARGET_NAME)"; 763 | SWIFT_VERSION = 5.0; 764 | TARGETED_DEVICE_FAMILY = "1,2"; 765 | }; 766 | name = Release; 767 | }; 768 | E2B5F9F6252D7BEA0018293C /* Debug */ = { 769 | isa = XCBuildConfiguration; 770 | buildSettings = { 771 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 772 | BUNDLE_LOADER = "$(TEST_HOST)"; 773 | CODE_SIGN_STYLE = Automatic; 774 | DEVELOPMENT_TEAM = E95KU8J3PN; 775 | INFOPLIST_FILE = ModernCollectionViewsTests/Info.plist; 776 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 777 | LD_RUNPATH_SEARCH_PATHS = ( 778 | "$(inherited)", 779 | "@executable_path/Frameworks", 780 | "@loader_path/Frameworks", 781 | ); 782 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViewsTests; 783 | PRODUCT_NAME = "$(TARGET_NAME)"; 784 | SWIFT_VERSION = 5.0; 785 | TARGETED_DEVICE_FAMILY = "1,2"; 786 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModernCollectionViews.app/ModernCollectionViews"; 787 | }; 788 | name = Debug; 789 | }; 790 | E2B5F9F7252D7BEA0018293C /* Release */ = { 791 | isa = XCBuildConfiguration; 792 | buildSettings = { 793 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 794 | BUNDLE_LOADER = "$(TEST_HOST)"; 795 | CODE_SIGN_STYLE = Automatic; 796 | DEVELOPMENT_TEAM = E95KU8J3PN; 797 | INFOPLIST_FILE = ModernCollectionViewsTests/Info.plist; 798 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 799 | LD_RUNPATH_SEARCH_PATHS = ( 800 | "$(inherited)", 801 | "@executable_path/Frameworks", 802 | "@loader_path/Frameworks", 803 | ); 804 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViewsTests; 805 | PRODUCT_NAME = "$(TARGET_NAME)"; 806 | SWIFT_VERSION = 5.0; 807 | TARGETED_DEVICE_FAMILY = "1,2"; 808 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ModernCollectionViews.app/ModernCollectionViews"; 809 | }; 810 | name = Release; 811 | }; 812 | E2B5F9F9252D7BEA0018293C /* Debug */ = { 813 | isa = XCBuildConfiguration; 814 | buildSettings = { 815 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 816 | CODE_SIGN_STYLE = Automatic; 817 | DEVELOPMENT_TEAM = E95KU8J3PN; 818 | INFOPLIST_FILE = ModernCollectionViewsUITests/Info.plist; 819 | LD_RUNPATH_SEARCH_PATHS = ( 820 | "$(inherited)", 821 | "@executable_path/Frameworks", 822 | "@loader_path/Frameworks", 823 | ); 824 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViewsUITests; 825 | PRODUCT_NAME = "$(TARGET_NAME)"; 826 | SWIFT_VERSION = 5.0; 827 | TARGETED_DEVICE_FAMILY = "1,2"; 828 | TEST_TARGET_NAME = ModernCollectionViews; 829 | }; 830 | name = Debug; 831 | }; 832 | E2B5F9FA252D7BEA0018293C /* Release */ = { 833 | isa = XCBuildConfiguration; 834 | buildSettings = { 835 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 836 | CODE_SIGN_STYLE = Automatic; 837 | DEVELOPMENT_TEAM = E95KU8J3PN; 838 | INFOPLIST_FILE = ModernCollectionViewsUITests/Info.plist; 839 | LD_RUNPATH_SEARCH_PATHS = ( 840 | "$(inherited)", 841 | "@executable_path/Frameworks", 842 | "@loader_path/Frameworks", 843 | ); 844 | PRODUCT_BUNDLE_IDENTIFIER = com.Alonso.ModernCollectionViewsUITests; 845 | PRODUCT_NAME = "$(TARGET_NAME)"; 846 | SWIFT_VERSION = 5.0; 847 | TARGETED_DEVICE_FAMILY = "1,2"; 848 | TEST_TARGET_NAME = ModernCollectionViews; 849 | }; 850 | name = Release; 851 | }; 852 | /* End XCBuildConfiguration section */ 853 | 854 | /* Begin XCConfigurationList section */ 855 | E2B5F9C3252D7BE60018293C /* Build configuration list for PBXProject "ModernCollectionViews" */ = { 856 | isa = XCConfigurationList; 857 | buildConfigurations = ( 858 | E2B5F9F0252D7BEA0018293C /* Debug */, 859 | E2B5F9F1252D7BEA0018293C /* Release */, 860 | ); 861 | defaultConfigurationIsVisible = 0; 862 | defaultConfigurationName = Release; 863 | }; 864 | E2B5F9F2252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViews" */ = { 865 | isa = XCConfigurationList; 866 | buildConfigurations = ( 867 | E2B5F9F3252D7BEA0018293C /* Debug */, 868 | E2B5F9F4252D7BEA0018293C /* Release */, 869 | ); 870 | defaultConfigurationIsVisible = 0; 871 | defaultConfigurationName = Release; 872 | }; 873 | E2B5F9F5252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViewsTests" */ = { 874 | isa = XCConfigurationList; 875 | buildConfigurations = ( 876 | E2B5F9F6252D7BEA0018293C /* Debug */, 877 | E2B5F9F7252D7BEA0018293C /* Release */, 878 | ); 879 | defaultConfigurationIsVisible = 0; 880 | defaultConfigurationName = Release; 881 | }; 882 | E2B5F9F8252D7BEA0018293C /* Build configuration list for PBXNativeTarget "ModernCollectionViewsUITests" */ = { 883 | isa = XCConfigurationList; 884 | buildConfigurations = ( 885 | E2B5F9F9252D7BEA0018293C /* Debug */, 886 | E2B5F9FA252D7BEA0018293C /* Release */, 887 | ); 888 | defaultConfigurationIsVisible = 0; 889 | defaultConfigurationName = Release; 890 | }; 891 | /* End XCConfigurationList section */ 892 | }; 893 | rootObject = E2B5F9C0252D7BE60018293C /* Project object */; 894 | } 895 | -------------------------------------------------------------------------------- /ModernCollectionViews.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ModernCollectionViews.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ModernCollectionViews/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/6/20. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | return true 15 | } 16 | 17 | // MARK: UISceneSession Lifecycle 18 | 19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 21 | } 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /ModernCollectionViews/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ModernCollectionViews/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ModernCollectionViews/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ModernCollectionViews/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ModernCollectionViews/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /ModernCollectionViews/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/6/20. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | var mainCoordinator: Coordinator! 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | guard let windowScene = (scene as? UIWindowScene) else { return } 17 | self.window = UIWindow(windowScene: windowScene) 18 | 19 | mainCoordinator = MainCoordinator(navigationController: UINavigationController()) 20 | mainCoordinator.start() 21 | self.window?.rootViewController = mainCoordinator.navigationController 22 | 23 | self.window?.makeKeyAndVisible() 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CellRegistration/CellRegistrationCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellRegistrationCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol CellRegistrationCoordinatorProtocol: Coordinator {} 11 | 12 | final class CellRegistrationCoordinator: CellRegistrationCoordinatorProtocol { 13 | 14 | var childCoordinators: [Coordinator] = [] 15 | var parentCoordinator: Coordinator? 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let viewController = CellRegistrationViewController() 24 | 25 | viewController.coordinator = self 26 | 27 | navigationController.pushViewController(viewController, animated: true) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CellRegistration/CellRegistrationTopicContentConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellRegistrationContentConfiguration.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | enum CellRegistrationContentConfiguration: CaseIterable { 11 | 12 | case subtitle, value 13 | 14 | var title: String { 15 | switch self { 16 | case .subtitle: 17 | return "Subtitle cell" 18 | case .value: 19 | return "Value cell" 20 | } 21 | } 22 | 23 | var configuration: UIListContentConfiguration { 24 | switch self { 25 | case .subtitle: 26 | return .subtitleCell() 27 | case .value: 28 | return .valueCell() 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CellRegistration/CellRegistrationTopicViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellRegistrationTopicViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class CellRegistrationViewController: UICollectionViewController { 11 | 12 | lazy private var configBarButtonItem: UIBarButtonItem = { 13 | let menu = createContentConfigurationMenu() 14 | return UIBarButtonItem(image: UIImage(systemName: "gear"), menu: menu) 15 | }() 16 | 17 | // MARK: - Properties 18 | 19 | private var dataSource: UICollectionViewDiffableDataSource! 20 | 21 | private var mainItems: [Int] = Array(0..<5) 22 | private var secondaryItems: [Int] = Array(5..<15) 23 | 24 | weak var coordinator: CellRegistrationTopicCoordinatorProtocol? 25 | 26 | // MARK: - Initializers 27 | 28 | init() { 29 | super.init(collectionViewLayout: UICollectionViewLayout()) 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | super.init(coder: coder) 34 | } 35 | 36 | // MARK: - Lifecycle 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | collectionView.backgroundColor = .systemBackground 41 | 42 | setupNavigationBar() 43 | setupCollectionView() 44 | } 45 | 46 | // MARK: - Private 47 | 48 | private func setupNavigationBar() { 49 | title = "Cell registration" 50 | navigationItem.rightBarButtonItems = [configBarButtonItem, editButtonItem] 51 | } 52 | 53 | private func setupCollectionView() { 54 | collectionView.collectionViewLayout = makeCollectionViewLayout() 55 | 56 | configureDataSource(with: .valueCell()) 57 | updateUI() 58 | } 59 | 60 | private func makeCollectionViewLayout() -> UICollectionViewLayout { 61 | var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 62 | 63 | config.trailingSwipeActionsConfigurationProvider = { indexPath in 64 | let actionHandler: UIContextualAction.Handler = { action, view, completion in 65 | completion(true) 66 | } 67 | 68 | let action = UIContextualAction(style: .normal, title: "Test", handler: actionHandler) 69 | action.image = UIImage(systemName: "checkmark") 70 | action.backgroundColor = .systemGreen 71 | 72 | return UISwipeActionsConfiguration(actions: [action]) 73 | } 74 | 75 | return UICollectionViewCompositionalLayout.list(using: config) 76 | } 77 | 78 | private func configureDataSource(with contentConfiguration: UIListContentConfiguration) { 79 | 80 | // Cell Registration 81 | 82 | let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 83 | 84 | var contentConfiguration = contentConfiguration 85 | contentConfiguration.text = "Row \(item)" 86 | contentConfiguration.secondaryText = "Value" 87 | contentConfiguration.image = UIImage(systemName: "applelogo") 88 | 89 | cell.accessories = [ 90 | .disclosureIndicator(), 91 | .insert(displayed: .whenEditing), 92 | .delete(displayed: .whenEditing)] 93 | 94 | cell.contentConfiguration = contentConfiguration 95 | } 96 | 97 | // Diffable Data Source 98 | 99 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 100 | (collectionView, indexPath, identifier) -> UICollectionViewCell? in 101 | 102 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) 103 | } 104 | 105 | // Reordering 106 | 107 | dataSource.reorderingHandlers.canReorderItem = { _ in return true } 108 | dataSource.reorderingHandlers.didReorder = { [weak self] transaction in 109 | guard let self = self else { return } 110 | for sectionTransaction in transaction.sectionTransactions { 111 | let sectionIdentifier = sectionTransaction.sectionIdentifier 112 | switch sectionIdentifier { 113 | case .main: 114 | self.mainItems = sectionTransaction.finalSnapshot.items 115 | case .secondary: 116 | self.secondaryItems = sectionTransaction.finalSnapshot.items 117 | } 118 | } 119 | } 120 | } 121 | 122 | private func updateUI(animated: Bool = false) { 123 | var snapshot = NSDiffableDataSourceSnapshot() 124 | snapshot.appendSections([.main, .secondary]) 125 | snapshot.appendItems(mainItems, toSection: .main) 126 | snapshot.appendItems(secondaryItems, toSection: .secondary) 127 | dataSource?.apply(snapshot, animatingDifferences: animated) 128 | } 129 | 130 | private func createContentConfigurationMenu() -> UIMenu { 131 | var actions: [UIAction] = [] 132 | CellRegistrationTopicContentConfiguration.allCases.forEach { content in 133 | let action = UIAction(title: content.title, 134 | image: UIImage(systemName: "paperplane")) { [weak self] _ in 135 | guard let self = self else { return } 136 | self.configureDataSource(with: content.configuration) 137 | self.updateUI() 138 | } 139 | actions.append(action) 140 | } 141 | return UIMenu(title: "Content configuration", children: actions) 142 | } 143 | 144 | } 145 | 146 | // MARK: - Sections 147 | 148 | extension CellRegistrationViewController { 149 | 150 | enum Section { 151 | case main 152 | case secondary 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CellRegistration/CellRegistrationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CellRegistrationViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class CellRegistrationViewController: UICollectionViewController { 11 | 12 | lazy private var configBarButtonItem: UIBarButtonItem = { 13 | let menu = createContentConfigurationMenu() 14 | return UIBarButtonItem(image: UIImage(systemName: "gear"), menu: menu) 15 | }() 16 | 17 | // MARK: - Properties 18 | 19 | private var dataSource: UICollectionViewDiffableDataSource! 20 | 21 | weak var coordinator: CellRegistrationCoordinatorProtocol? 22 | 23 | // MARK: - Initializers 24 | 25 | init() { 26 | super.init(collectionViewLayout: UICollectionViewLayout()) 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | } 32 | 33 | // MARK: - Lifecycle 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | collectionView.backgroundColor = .systemBackground 38 | 39 | setupNavigationBar() 40 | setupCollectionView() 41 | } 42 | 43 | // MARK: - Private 44 | 45 | private func setupNavigationBar() { 46 | title = "Cell registration" 47 | navigationItem.rightBarButtonItems = [configBarButtonItem, editButtonItem] 48 | } 49 | 50 | private func setupCollectionView() { 51 | collectionView.collectionViewLayout = makeCollectionViewLayout() 52 | 53 | configureDataSource(with: .valueCell()) 54 | updateUI() 55 | } 56 | 57 | private func makeCollectionViewLayout() -> UICollectionViewLayout { 58 | var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 59 | 60 | config.trailingSwipeActionsConfigurationProvider = { indexPath in 61 | let actionHandler: UIContextualAction.Handler = { action, view, completion in 62 | completion(true) 63 | } 64 | 65 | let action = UIContextualAction(style: .normal, title: "Test", handler: actionHandler) 66 | action.image = UIImage(systemName: "checkmark") 67 | action.backgroundColor = .systemGreen 68 | 69 | return UISwipeActionsConfiguration(actions: [action]) 70 | } 71 | 72 | return UICollectionViewCompositionalLayout.list(using: config) 73 | } 74 | 75 | private func configureDataSource(with contentConfiguration: UIListContentConfiguration) { 76 | 77 | // Cell Registration 78 | 79 | let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 80 | 81 | var contentConfiguration = contentConfiguration 82 | contentConfiguration.text = "Row \(item)" 83 | contentConfiguration.secondaryText = "Value" 84 | contentConfiguration.image = UIImage(systemName: "applelogo") 85 | 86 | cell.accessories = [ 87 | .disclosureIndicator(), 88 | .insert(displayed: .whenEditing), 89 | .delete(displayed: .whenEditing)] 90 | 91 | cell.contentConfiguration = contentConfiguration 92 | } 93 | 94 | // Diffable Data Source 95 | 96 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 97 | (collectionView, indexPath, identifier) -> UICollectionViewCell? in 98 | 99 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) 100 | } 101 | 102 | } 103 | 104 | private func updateUI(animated: Bool = false) { 105 | var snapshot = NSDiffableDataSourceSnapshot() 106 | snapshot.appendSections([.main, .secondary]) 107 | snapshot.appendItems(Section.main.items, toSection: .main) 108 | snapshot.appendItems(Section.secondary.items, toSection: .secondary) 109 | dataSource?.apply(snapshot, animatingDifferences: animated) 110 | } 111 | 112 | private func createContentConfigurationMenu() -> UIMenu { 113 | var actions: [UIAction] = [] 114 | CellRegistrationContentConfiguration.allCases.forEach { content in 115 | let action = UIAction(title: content.title, 116 | image: UIImage(systemName: "paperplane")) { [weak self] _ in 117 | guard let self = self else { return } 118 | self.configureDataSource(with: content.configuration) 119 | self.updateUI() 120 | } 121 | actions.append(action) 122 | } 123 | return UIMenu(title: "Content configuration", children: actions) 124 | } 125 | 126 | } 127 | 128 | // MARK: - Sections 129 | 130 | extension CellRegistrationViewController { 131 | 132 | enum Section { 133 | case main 134 | case secondary 135 | 136 | var items: [Int] { 137 | switch self { 138 | case .main: 139 | return Array(0..<5) 140 | case .secondary: 141 | return Array(5..<15) 142 | } 143 | } 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/CompositionalLayoutProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutProtocols.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/14/20. 6 | // 7 | 8 | protocol CompositionalLayoutTopicViewFactoryProtocol { 9 | func makeCollectionViewLayouts() -> [LayoutProtocol] 10 | } 11 | 12 | protocol CompositionalLayoutTopicCoordinatorProtocol: AnyObject {} 13 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/CompositionalLayoutTopicCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutTopicCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CompositionalLayoutTopicCoordinator: Coordinator, CompositionalLayoutTopicCoordinatorProtocol { 11 | 12 | var childCoordinators: [Coordinator] = [] 13 | var parentCoordinator: Coordinator? 14 | var navigationController: UINavigationController 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let factory = CompositionalLayoutTopicViewFactory() 22 | let viewController = CompositionalLayoutTopicViewController(factory: factory) 23 | 24 | viewController.coordinator = self 25 | 26 | navigationController.pushViewController(viewController, animated: true) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/CompositionalLayoutTopicViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutTopicViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CompositionalLayoutTopicViewController: UICollectionViewController { 11 | 12 | private let factory: CompositionalLayoutTopicViewFactoryProtocol 13 | 14 | weak var coordinator: CompositionalLayoutTopicCoordinatorProtocol? 15 | 16 | private var layouts: [LayoutProtocol] { 17 | return factory.makeCollectionViewLayouts() 18 | } 19 | 20 | private var currentLayoutIndex = 0 { 21 | didSet { 22 | if currentLayoutIndex > layouts.count - 1 { 23 | currentLayoutIndex = 0 24 | } 25 | } 26 | } 27 | 28 | // MARK: - Initializers 29 | 30 | init(factory: CompositionalLayoutTopicViewFactoryProtocol) { 31 | self.factory = factory 32 | super.init(collectionViewLayout: UICollectionViewLayout()) 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | // MARK: - Lifecycle 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | title = "Compositional layout" 44 | collectionView.backgroundColor = .systemBackground 45 | 46 | navigationItem.rightBarButtonItem = UIBarButtonItem( 47 | barButtonSystemItem: .fastForward, target: self, 48 | action: #selector(updateLayout)) 49 | 50 | configureCollectionView() 51 | } 52 | 53 | // MARK: - Private 54 | 55 | private func configureCollectionView() { 56 | collectionView.dataSource = self 57 | collectionView.register(cellType: NumberedCollectionViewCell.self) 58 | 59 | let initialLayout = layouts[0] 60 | navigationItem.prompt = initialLayout.title 61 | collectionView.collectionViewLayout = initialLayout.makeLayout() 62 | } 63 | 64 | // MARK: - Selectors 65 | 66 | @objc private func updateLayout() { 67 | currentLayoutIndex += 1 68 | let layout = layouts[currentLayoutIndex] 69 | navigationItem.prompt = layout.title 70 | collectionView.setCollectionViewLayout(layout.makeLayout(), animated: true) 71 | } 72 | 73 | // MARK: - UICollectionViewDataSource 74 | 75 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 76 | return 1 77 | } 78 | 79 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 80 | return 16 81 | } 82 | 83 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 84 | let cell = collectionView.dequeueReusableCell(with: NumberedCollectionViewCell.self, for: indexPath) 85 | cell.number = indexPath.row 86 | 87 | return cell 88 | } 89 | 90 | override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 91 | return collectionView.dequeueReusableView(with: UICollectionReusableView.self, kind: kind, for: indexPath) 92 | } 93 | 94 | } 95 | 96 | // MARK: - Sections 97 | 98 | extension CompositionalLayoutTopicViewController { 99 | 100 | enum Section { 101 | case main 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/CompositionalLayoutTopicViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalLayoutTopicViewFactory.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/5/20. 6 | // 7 | 8 | import UIKit 9 | 10 | struct CompositionalLayoutTopicViewFactory: CompositionalLayoutTopicViewFactoryProtocol { 11 | 12 | func makeCollectionViewLayouts() -> [LayoutProtocol] { 13 | return [SquareLayout(), NestedGroupLayout(), NestedGroupV2Layout(), ListLayout()] 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/Layouts/LayoutProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutProtocol.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol LayoutProtocol { 11 | 12 | var title: String { get } 13 | 14 | func makeLayout() -> UICollectionViewLayout 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/Layouts/ListLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListLayout.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class ListLayout: LayoutProtocol { 11 | 12 | var title: String { 13 | return "List" 14 | } 15 | 16 | func makeLayout() -> UICollectionViewLayout { 17 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 18 | heightDimension: .fractionalHeight(1.0)) 19 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 20 | item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 21 | 22 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 23 | heightDimension: .absolute(100)) 24 | let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, 25 | subitems: [item]) 26 | 27 | let section = NSCollectionLayoutSection(group: group) 28 | 29 | return UICollectionViewCompositionalLayout(section: section) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/Layouts/NestedGroupLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NestedGroupLayout.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class NestedGroupLayout: LayoutProtocol { 11 | 12 | var title: String { 13 | return "Nested groups" 14 | } 15 | 16 | private var topItem: NSCollectionLayoutItem { 17 | let topItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 18 | heightDimension: .fractionalWidth(0.5)) 19 | let topItem = NSCollectionLayoutItem(layoutSize: topItemSize) 20 | topItem.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 21 | 22 | return topItem 23 | } 24 | 25 | private var bottomItem: NSCollectionLayoutItem { 26 | let bottomItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)) 27 | let bottomItem = NSCollectionLayoutItem(layoutSize: bottomItemSize) 28 | bottomItem.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 29 | 30 | return bottomItem 31 | } 32 | 33 | func makeLayout() -> UICollectionViewLayout { 34 | 35 | // Group for bottom item, it repeats the bottom item twice 36 | let bottomGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 37 | heightDimension: .fractionalWidth(0.5)) 38 | let bottomGroup = NSCollectionLayoutGroup.horizontal(layoutSize: bottomGroupSize, 39 | subitem: bottomItem, count: 2) 40 | 41 | // Combine the top item and bottom group 42 | let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 43 | heightDimension: .fractionalWidth(1)) 44 | let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, 45 | subitems: [topItem, bottomGroup]) 46 | 47 | let section = NSCollectionLayoutSection(group: nestedGroup) 48 | 49 | let layout = UICollectionViewCompositionalLayout(section: section) 50 | 51 | return layout 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/Layouts/NestedGroupV2Layout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NestedGroupV2Layout.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class NestedGroupV2Layout: LayoutProtocol { 11 | 12 | var title: String { 13 | return "Nested group v2" 14 | } 15 | 16 | func makeLayout() -> UICollectionViewLayout { 17 | let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in 18 | let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1.0))) 19 | leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0) 20 | 21 | let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.5))) 22 | trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0) 23 | 24 | let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0)), subitem: trailingItem, count: 3) 25 | 26 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.4)), subitems: [leadingItem, trailingGroup]) 27 | 28 | let section = NSCollectionLayoutSection(group: group) 29 | section.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5.0, bottom: 5.0, trailing: 5.0) 30 | 31 | return section 32 | } 33 | 34 | return layout 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/CompositionalLayout/Layouts/SquareLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SquareLayout.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class SquareLayout: LayoutProtocol { 11 | 12 | var title: String { 13 | return "Square" 14 | } 15 | 16 | func makeLayout() -> UICollectionViewLayout { 17 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), 18 | heightDimension: .fractionalHeight(1.0)) 19 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 20 | item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 21 | 22 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 23 | heightDimension: .fractionalWidth(0.5)) 24 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 25 | group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 4, bottom: 0, trailing: 4) 26 | 27 | let section = NSCollectionLayoutSection(group: group) 28 | section.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0) 29 | 30 | return UICollectionViewCompositionalLayout(section: section) 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/BluetoothSettings/BluetoothItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothItem.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum BluetoothItem: Hashable { 11 | case enable, available(title: String) 12 | 13 | var title: String { 14 | switch self { 15 | case .enable: 16 | return "Bluetooth" 17 | case .available(let title): 18 | return title 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/BluetoothSettings/BluetoothSettingsCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothSettingsCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol BluetoothSettingsCoordinatorProtocol: Coordinator {} 11 | 12 | final class BluetoothSettingsCoordinator: BluetoothSettingsCoordinatorProtocol { 13 | 14 | var childCoordinators: [Coordinator] = [] 15 | var parentCoordinator: Coordinator? 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let viewController = BluetoothSettingsViewController() 24 | 25 | viewController.coordinator = self 26 | 27 | navigationController.pushViewController(viewController, animated: true) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/BluetoothSettings/BluetoothSettingsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluetoothSettingsViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class BluetoothSettingsViewController: UICollectionViewController { 11 | 12 | // MARK: - Properties 13 | 14 | private var isShowingAvailableConnections = true 15 | 16 | private var dataSource: UICollectionViewDiffableDataSource! 17 | 18 | weak var coordinator: BluetoothSettingsCoordinatorProtocol? 19 | 20 | // MARK: - Initializers 21 | 22 | init() { 23 | super.init(collectionViewLayout: UICollectionViewLayout()) 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | } 29 | 30 | // MARK: - Lifecycle 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | collectionView.allowsSelection = false 35 | collectionView.backgroundColor = .systemBackground 36 | 37 | setupNavigationBar() 38 | setupCollectionView() 39 | } 40 | 41 | // MARK: - Private 42 | 43 | private func setupNavigationBar() { 44 | title = "Bluetooth settings" 45 | } 46 | 47 | private func setupCollectionView() { 48 | collectionView.collectionViewLayout = makeCollectionViewLayout() 49 | 50 | configureDataSource() 51 | showAvailableConnections() 52 | } 53 | 54 | private func makeCollectionViewLayout() -> UICollectionViewLayout { 55 | let config = UICollectionLayoutListConfiguration(appearance: .grouped) 56 | return UICollectionViewCompositionalLayout.list(using: config) 57 | } 58 | 59 | private func configureDataSource() { 60 | 61 | // Cell Registration 62 | 63 | let enableBluetoothCellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, item in 64 | guard let self = self else { fatalError() } 65 | 66 | var contentConfiguration = UIListContentConfiguration.cell() 67 | contentConfiguration.text = item.title 68 | 69 | let enableWifiSwitch = UISwitch() 70 | enableWifiSwitch.isOn = self.isShowingAvailableConnections 71 | enableWifiSwitch.addTarget(self, action: #selector(self.toggleBluetooth), for: .touchUpInside) 72 | cell.accessories = [ 73 | .customView(configuration: .init(customView: enableWifiSwitch, 74 | placement: .trailing())) 75 | ] 76 | 77 | cell.contentConfiguration = contentConfiguration 78 | } 79 | 80 | let availableConnectionsCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 81 | 82 | var contentConfiguration = UIListContentConfiguration.valueCell() 83 | contentConfiguration.text = item.title 84 | contentConfiguration.secondaryText = "Not connected" 85 | 86 | cell.contentConfiguration = contentConfiguration 87 | } 88 | 89 | // Diffable Data Source 90 | 91 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 92 | (collectionView, indexPath, item) -> UICollectionViewCell? in 93 | switch item { 94 | case .enable: 95 | return collectionView.dequeueConfiguredReusableCell( 96 | using: enableBluetoothCellRegistration, 97 | for: indexPath, 98 | item: item) 99 | case .available: 100 | return collectionView.dequeueConfiguredReusableCell( 101 | using: availableConnectionsCellRegistration, 102 | for: indexPath, 103 | item: item) 104 | } 105 | } 106 | } 107 | 108 | private func showAvailableConnections() { 109 | var snapshot = NSDiffableDataSourceSnapshot() 110 | snapshot.appendSections([.enableBluetooth]) 111 | snapshot.appendItems([.enable], toSection: .enableBluetooth) 112 | 113 | let availableConnections: [BluetoothItem] = [ 114 | .available(title: "Bluetooth Connection 1"), 115 | .available(title: "Bluetooth Connection 2"), 116 | .available(title: "Bluetooth Connection 3"), 117 | .available(title: "Bluetooth Connection 4"), 118 | .available(title: "Bluetooth Connection 5"), 119 | .available(title: "Bluetooth Connection 6") 120 | ] 121 | snapshot.appendSections([.availableConnections]) 122 | snapshot.appendItems(availableConnections, toSection: .availableConnections) 123 | 124 | dataSource?.apply(snapshot, animatingDifferences: true) 125 | } 126 | 127 | private func hideAvailableConnections() { 128 | var snapshot = NSDiffableDataSourceSnapshot() 129 | snapshot.appendSections([.enableBluetooth, .availableConnections]) 130 | 131 | snapshot.appendItems([.enable], toSection: .enableBluetooth) 132 | snapshot.appendItems([], toSection: .availableConnections) 133 | 134 | dataSource?.apply(snapshot, animatingDifferences: true) 135 | } 136 | 137 | 138 | @objc func toggleBluetooth() { 139 | isShowingAvailableConnections.toggle() 140 | isShowingAvailableConnections ? showAvailableConnections() : hideAvailableConnections() 141 | } 142 | 143 | } 144 | 145 | // MARK: - Sections 146 | 147 | extension BluetoothSettingsViewController { 148 | 149 | enum Section { 150 | case enableBluetooth 151 | case availableConnections 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/DiffableDataSourceCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableDataSourceCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol DiffableDataSourceCoordinatorProtocol: AnyObject { 11 | 12 | func showOddEvenNumbers() 13 | func showBluetoothSettings() 14 | 15 | } 16 | 17 | class DiffableDataSourceCoordinator: Coordinator, DiffableDataSourceCoordinatorProtocol { 18 | 19 | var childCoordinators: [Coordinator] = [] 20 | var parentCoordinator: Coordinator? 21 | var navigationController: UINavigationController 22 | 23 | init(navigationController: UINavigationController) { 24 | self.navigationController = navigationController 25 | } 26 | 27 | func start() { 28 | let viewController = DiffableDataSourceViewController() 29 | 30 | viewController.coordinator = self 31 | 32 | navigationController.pushViewController(viewController, animated: true) 33 | } 34 | 35 | func showOddEvenNumbers() { 36 | let coordinator = OddEvenNumbersCoordinator(navigationController: navigationController) 37 | 38 | coordinator.parentCoordinator = unwrappedParentCoordinator 39 | 40 | unwrappedParentCoordinator.childCoordinators.append(coordinator) 41 | coordinator.start() 42 | } 43 | 44 | func showBluetoothSettings() { 45 | let coordinator = BluetoothSettingsCoordinator(navigationController: navigationController) 46 | 47 | coordinator.parentCoordinator = unwrappedParentCoordinator 48 | 49 | unwrappedParentCoordinator.childCoordinators.append(coordinator) 50 | coordinator.start() 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/DiffableDataSourceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffableDataSourceViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/23/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class DiffableDataSourceViewController: UICollectionViewController { 11 | 12 | private var dataSource: UICollectionViewDiffableDataSource! 13 | 14 | weak var coordinator: DiffableDataSourceCoordinatorProtocol? 15 | 16 | // MARK: - Initializers 17 | 18 | init() { 19 | super.init(collectionViewLayout: UICollectionViewLayout()) 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError() 24 | } 25 | 26 | // MARK: - Lifecycle 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | title = "UICollectionViewDiffableDataSource" 31 | 32 | configureCollectionView() 33 | } 34 | 35 | // MARK: - Private 36 | 37 | private func configureCollectionView() { 38 | collectionView.register(viewType: SectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader) 39 | 40 | configureCollectionViewLayout() 41 | configureCollectionViewDataSource() 42 | updateUI() 43 | } 44 | 45 | private func configureCollectionViewLayout() { 46 | let config = UICollectionLayoutListConfiguration(appearance: .grouped) 47 | collectionView.collectionViewLayout = UICollectionViewCompositionalLayout.list(using: config) 48 | } 49 | 50 | private func configureCollectionViewDataSource() { 51 | let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 52 | var content = UIListContentConfiguration.cell() 53 | content.text = item 54 | 55 | cell.accessories = [.disclosureIndicator()] 56 | cell.contentConfiguration = content 57 | } 58 | 59 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 60 | (collectionView: UICollectionView, indexPath: IndexPath, identifier: String) -> UICollectionViewCell? in 61 | let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, 62 | for: indexPath, 63 | item: identifier) 64 | return cell 65 | } 66 | } 67 | 68 | private func updateUI() { 69 | var snapshot = NSDiffableDataSourceSnapshot() 70 | snapshot.appendSections([.main]) 71 | 72 | let items = ["Bluetooth settings", "Odd/Even numbers"] 73 | snapshot.appendItems(items, toSection: .main) 74 | dataSource?.apply(snapshot, animatingDifferences: false) 75 | } 76 | 77 | // MARK: - UICollectionViewDelegate 78 | 79 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 80 | collectionView.deselectItem(at: indexPath, animated: true) 81 | if indexPath.row == .zero { 82 | coordinator?.showBluetoothSettings() 83 | } else if indexPath.row == 1 { 84 | coordinator?.showOddEvenNumbers() 85 | } 86 | } 87 | 88 | } 89 | 90 | // MARK: - Sections 91 | 92 | extension DiffableDataSourceViewController { 93 | 94 | enum Section { 95 | case main 96 | } 97 | 98 | } 99 | 100 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OddEvenNumbersCoordinator: Coordinator, OddEvenNumbersCoordinatorProtocol { 11 | 12 | var childCoordinators: [Coordinator] = [] 13 | var parentCoordinator: Coordinator? 14 | var navigationController: UINavigationController 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let viewModel = OddEvenNumbersViewModel(numbers: Array(0..<100)) 22 | let factory = OddEvenNumbersViewFactory() 23 | let viewController = OddEvenNumbersViewController(viewModel: viewModel, 24 | factory: factory) 25 | 26 | viewController.coordinator = self 27 | 28 | navigationController.pushViewController(viewController, animated: true) 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersProtocols.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/14/20. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | protocol OddEvenNumbersViewModelProtocol { 12 | 13 | var numbersPublisher: Published<[Int]>.Publisher { get } 14 | 15 | func selectAllNumbers() 16 | func selectOddNumbers() 17 | func selectEvenNumbers() 18 | 19 | } 20 | 21 | protocol OddEvenNumbersCoordinatorProtocol: AnyObject {} 22 | 23 | protocol OddEvenNumbersViewFactoryProtocol { 24 | 25 | func makeCollectionViewLayout() -> UICollectionViewLayout 26 | 27 | } 28 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersView.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class OddEvenNumbersView: UIView { 11 | 12 | lazy var collectionView: UICollectionView = { 13 | let layout = UICollectionViewLayout() 14 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 15 | collectionView.translatesAutoresizingMaskIntoConstraints = false 16 | collectionView.backgroundColor = .white 17 | 18 | return collectionView 19 | }() 20 | 21 | lazy var segmentedControl: UISegmentedControl = { 22 | let segmentedControl = UISegmentedControl(items: ["All", "Odd", "Even"]) 23 | segmentedControl.backgroundColor = .systemGroupedBackground 24 | segmentedControl.selectedSegmentIndex = 0 25 | segmentedControl.translatesAutoresizingMaskIntoConstraints = false 26 | 27 | return segmentedControl 28 | }() 29 | 30 | // MARK: - Initializers 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | setupUI() 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | super.init(coder: coder) 39 | setupUI() 40 | } 41 | 42 | // MARK: - Private 43 | 44 | private func setupUI() { 45 | backgroundColor = .systemBackground 46 | 47 | setupCollectionView() 48 | setupSegmentedControl() 49 | } 50 | 51 | private func setupCollectionView() { 52 | collectionView.backgroundColor = .clear 53 | addSubview(collectionView) 54 | NSLayoutConstraint.activate([ 55 | collectionView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), 56 | collectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor), 57 | collectionView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), 58 | collectionView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) 59 | ]) 60 | } 61 | 62 | private func setupSegmentedControl() { 63 | addSubview(segmentedControl) 64 | NSLayoutConstraint.activate([ 65 | segmentedControl.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -8), 66 | segmentedControl.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 8), 67 | segmentedControl.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -8) 68 | ]) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | final class OddEvenNumbersViewController: UIViewController { 12 | 13 | enum Section { 14 | case main 15 | } 16 | 17 | private var topicView: OddEvenNumbersView! 18 | private let viewModel: OddEvenNumbersViewModelProtocol 19 | private let factory: OddEvenNumbersViewFactoryProtocol 20 | 21 | private var dataSource: UICollectionViewDiffableDataSource! 22 | private var currentSnapshot: NSDiffableDataSourceSnapshot! 23 | 24 | private var cancellables: Set = [] 25 | 26 | weak var coordinator: OddEvenNumbersCoordinatorProtocol? 27 | 28 | // MARK: - Initializers 29 | 30 | init(viewModel: OddEvenNumbersViewModelProtocol, 31 | factory: OddEvenNumbersViewFactoryProtocol) { 32 | self.viewModel = viewModel 33 | self.factory = factory 34 | super.init(nibName: nil, bundle: nil) 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError() 39 | } 40 | 41 | // MARK: - Lifycycle 42 | 43 | override func loadView() { 44 | topicView = OddEvenNumbersView() 45 | view = topicView 46 | } 47 | 48 | override func viewDidLoad() { 49 | super.viewDidLoad() 50 | title = "Odd/Even numbers" 51 | 52 | configureUI() 53 | configureBindings() 54 | 55 | viewModel.selectAllNumbers() 56 | } 57 | 58 | private func configureUI() { 59 | configureCollectionView() 60 | configureSegmentedControl() 61 | } 62 | 63 | private func configureCollectionView() { 64 | let collectionView = topicView.collectionView 65 | 66 | collectionView.register(cellType: NumberedCollectionViewCell.self) 67 | collectionView.collectionViewLayout = factory.makeCollectionViewLayout() 68 | 69 | configureCollectionViewDataSource() 70 | } 71 | 72 | private func configureSegmentedControl() { 73 | let segmentedControl = topicView.segmentedControl 74 | 75 | let all = UIAction(title: "All") { [weak self] _ in self?.viewModel.selectAllNumbers() } 76 | segmentedControl.setAction(all, forSegmentAt: 0) 77 | 78 | let even = UIAction(title: "Even") { [weak self] _ in self?.viewModel.selectEvenNumbers() } 79 | segmentedControl.setAction(even, forSegmentAt: 1) 80 | 81 | let odd = UIAction(title: "Odd") { [weak self] _ in self?.viewModel.selectOddNumbers() } 82 | segmentedControl.setAction(odd, forSegmentAt: 2) 83 | } 84 | 85 | // MARK: - Reactive Behaviour 86 | 87 | private func configureBindings() { 88 | viewModel.numbersPublisher 89 | .receive(on: RunLoop.main) 90 | .sink { [weak self] (numbers) in 91 | self?.updateUI(with: numbers) 92 | }.store(in: &cancellables) 93 | } 94 | 95 | // MARK: - Diffable Data Source 96 | 97 | private func configureCollectionViewDataSource() { 98 | let collectionView = topicView.collectionView 99 | 100 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 101 | (collectionView, indexPath, number) -> UICollectionViewCell? in 102 | let cell = collectionView.dequeueReusableCell(with: NumberedCollectionViewCell.self, 103 | for: indexPath) 104 | cell.number = number 105 | return cell 106 | } 107 | } 108 | 109 | // MARK: - Diffable Data Source Snapshot 110 | 111 | private func updateUI(with numbers: [Int], animated: Bool = true) { 112 | currentSnapshot = NSDiffableDataSourceSnapshot() 113 | currentSnapshot.appendSections([.main]) 114 | currentSnapshot.appendItems(numbers, toSection: .main) 115 | 116 | dataSource.apply(currentSnapshot, animatingDifferences: animated) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersViewFactory.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/5/20. 6 | // 7 | 8 | import UIKit 9 | 10 | struct OddEvenNumbersViewFactory: OddEvenNumbersViewFactoryProtocol { 11 | 12 | private var topItem: NSCollectionLayoutItem { 13 | let topItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 14 | heightDimension: .fractionalWidth(0.5)) 15 | let topItem = NSCollectionLayoutItem(layoutSize: topItemSize) 16 | topItem.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 17 | 18 | return topItem 19 | } 20 | 21 | private var bottomItem: NSCollectionLayoutItem { 22 | let bottomItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)) 23 | let bottomItem = NSCollectionLayoutItem(layoutSize: bottomItemSize) 24 | bottomItem.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 25 | 26 | return bottomItem 27 | } 28 | 29 | func makeCollectionViewLayout() -> UICollectionViewLayout { 30 | 31 | // Group for bottom item, it repeats the bottom item twice 32 | let bottomGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 33 | heightDimension: .fractionalWidth(0.5)) 34 | let bottomGroup = NSCollectionLayoutGroup.horizontal(layoutSize: bottomGroupSize, 35 | subitem: bottomItem, count: 2) 36 | 37 | // Combine the top item and bottom group 38 | let nestedGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 39 | heightDimension: .fractionalWidth(1)) 40 | let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: nestedGroupSize, 41 | subitems: [topItem, bottomGroup]) 42 | 43 | let section = NSCollectionLayoutSection(group: nestedGroup) 44 | 45 | let layout = UICollectionViewCompositionalLayout(section: section) 46 | 47 | return layout 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/DiffableDataSource/OddEvenNumbers/OddEvenNumbersViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OddEvenNumbersViewModel.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/12/20. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | final class OddEvenNumbersViewModel: OddEvenNumbersViewModelProtocol { 12 | 13 | @Published var numbersToDisplay: [Int] = [] 14 | 15 | var numbersPublisher: Published<[Int]>.Publisher { 16 | $numbersToDisplay 17 | } 18 | 19 | private let numbers: [Int] 20 | 21 | init(numbers: [Int]) { 22 | self.numbers = numbers 23 | } 24 | 25 | func selectAllNumbers() { 26 | numbersToDisplay = numbers 27 | } 28 | 29 | func selectOddNumbers() { 30 | numbersToDisplay = numbers.oddNumbers 31 | } 32 | 33 | func selectEvenNumbers() { 34 | numbersToDisplay = numbers.evenNumbers 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Lists/Extensions/Appearance+Title.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Appearance+Title.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionLayoutListConfiguration.Appearance { 11 | 12 | var title: String? { 13 | switch self { 14 | case .grouped: return "Grouped" 15 | case .insetGrouped: return "Inset grouped" 16 | case .plain: return "Plain" 17 | case .sidebar: return "Sidebar" 18 | case .sidebarPlain: return "Sidebar plain" 19 | @unknown default: return "Unknown" 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Lists/ListTopicCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListTopicCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ListTopicCoordinatorProtocol: Coordinator {} 11 | 12 | final class ListTopicCoordinator: ListTopicCoordinatorProtocol { 13 | 14 | var childCoordinators: [Coordinator] = [] 15 | var parentCoordinator: Coordinator? 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let viewController = ListsTopicViewController() 24 | 25 | viewController.coordinator = self 26 | 27 | navigationController.pushViewController(viewController, animated: true) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Lists/ListsTopicContentAppearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListsTopicContentConfiguration.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | enum ListsTopicContentAppearance: CaseIterable { 11 | 12 | case plain, grouped, insetGrouped 13 | 14 | var title: String? { 15 | switch self { 16 | case .plain: 17 | return "Plain" 18 | case .grouped: 19 | return "Grouped" 20 | case .insetGrouped: 21 | return "Inset grouped" 22 | } 23 | } 24 | 25 | var appearance: UICollectionLayoutListConfiguration.Appearance { 26 | switch self { 27 | case .plain: 28 | return .plain 29 | case .grouped: 30 | return .grouped 31 | case .insetGrouped: 32 | return .insetGrouped 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Lists/ListsTopicViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListsTopicViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/5/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ListsTopicViewController: UICollectionViewController { 11 | 12 | typealias Appearance = UICollectionLayoutListConfiguration.Appearance 13 | 14 | private var appearances: [Appearance] = [.grouped, .insetGrouped, .plain] 15 | 16 | private var currentAppearanceIndex = 0 { 17 | didSet { 18 | if currentAppearanceIndex > appearances.count - 1 { 19 | currentAppearanceIndex = 0 20 | } 21 | } 22 | } 23 | 24 | private var currentAppearance: Appearance { 25 | appearances[currentAppearanceIndex] 26 | } 27 | 28 | weak var coordinator: ListTopicCoordinatorProtocol? 29 | 30 | // MARK: - Initializers 31 | 32 | init() { 33 | super.init(collectionViewLayout: UICollectionViewLayout()) 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | super.init(coder: coder) 38 | } 39 | 40 | // MARK: - Lifecycle 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | collectionView.backgroundColor = .systemBackground 45 | 46 | setupNavigationBar() 47 | setupCollectionView() 48 | } 49 | 50 | // MARK: - Private 51 | 52 | private func setupNavigationBar() { 53 | title = "Lists" 54 | navigationItem.prompt = currentAppearance.title 55 | navigationItem.rightBarButtonItem = UIBarButtonItem( 56 | barButtonSystemItem: .fastForward, target: self, 57 | action: #selector(updateContentAppearance)) 58 | } 59 | 60 | private func setupCollectionView() { 61 | collectionView.dataSource = self 62 | collectionView.register(cellType: UICollectionViewListCell.self) 63 | 64 | collectionView.collectionViewLayout = makeCollectionViewLayout(using: currentAppearance) 65 | } 66 | 67 | private func makeCollectionViewLayout(using appearance: UICollectionLayoutListConfiguration.Appearance) -> UICollectionViewLayout { 68 | let configuration = UICollectionLayoutListConfiguration(appearance: appearance) 69 | return UICollectionViewCompositionalLayout.list(using: configuration) 70 | } 71 | 72 | // MARK: - Selectors 73 | 74 | @objc private func updateContentAppearance() { 75 | currentAppearanceIndex += 1 76 | 77 | let layout = self.makeCollectionViewLayout(using: currentAppearance) 78 | collectionView.setCollectionViewLayout(layout, animated: true) { [weak self] _ in 79 | guard let self = self else { return } 80 | self.navigationItem.prompt = self.currentAppearance.title 81 | } 82 | } 83 | 84 | } 85 | 86 | // MARK: - UICollectionViewDataSource 87 | 88 | extension ListsTopicViewController { 89 | 90 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 91 | return Section.allCases.count 92 | } 93 | 94 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 95 | let section = Section.allCases[section] 96 | return section.items.count 97 | } 98 | 99 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 100 | let cell = collectionView.dequeueReusableCell(with: UICollectionViewListCell.self, 101 | for: indexPath) 102 | 103 | let section = Section.allCases[indexPath.section] 104 | let item = section.items[indexPath.row] 105 | 106 | let tableviewCell = UITableViewCell(style: .value1, reuseIdentifier: "Cell") 107 | tableviewCell.textLabel?.text = "Row \(item)" 108 | tableviewCell.detailTextLabel?.text = "Value" 109 | tableviewCell.imageView?.image = UIImage(systemName: "applelogo") 110 | 111 | var contentConfiguration = UIListContentConfiguration.valueCell() 112 | contentConfiguration.text = "Row \(item)" 113 | contentConfiguration.secondaryText = "Value" 114 | contentConfiguration.image = UIImage(systemName: "applelogo") 115 | 116 | cell.contentConfiguration = contentConfiguration 117 | 118 | return cell 119 | } 120 | 121 | } 122 | 123 | // MARK: - Sections 124 | 125 | extension ListsTopicViewController { 126 | 127 | enum Section: CaseIterable { 128 | case main 129 | case secondary 130 | 131 | var items: [Int] { 132 | switch self { 133 | case .main: 134 | return Array(0..<10) 135 | case .secondary: 136 | return Array(11..<40) 137 | } 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/MainCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/8/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MainCoordinator: NSObject, Coordinator, MainCoordinatorProtocol { 11 | 12 | var childCoordinators: [Coordinator] = [] 13 | var parentCoordinator: Coordinator? 14 | var navigationController: UINavigationController 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let factory = MainViewFactory() 22 | let viewController = MainViewController(factory: factory) 23 | 24 | viewController.coordinator = self 25 | 26 | navigationController.delegate = self 27 | navigationController.pushViewController(viewController, animated: true) 28 | } 29 | 30 | func showTopic(_ topic: Topic) { 31 | let coordinator: Coordinator 32 | 33 | switch topic { 34 | case .collectionViewLists: 35 | coordinator = ListTopicCoordinator(navigationController: navigationController) 36 | case .compositionalLayouts: 37 | coordinator = CompositionalLayoutTopicCoordinator(navigationController: navigationController) 38 | case .diffableDataSources: 39 | coordinator = DiffableDataSourceCoordinator(navigationController: navigationController) 40 | case .cellRegistration: 41 | coordinator = CellRegistrationCoordinator(navigationController: navigationController) 42 | case .multipleLayouts: 43 | coordinator = MultipleLayoutsCoordinator(navigationController: navigationController) 44 | case .otherCapabilities: 45 | coordinator = OtherCapabilitiesCoordinator(navigationController: navigationController) 46 | } 47 | 48 | coordinator.parentCoordinator = unwrappedParentCoordinator 49 | 50 | unwrappedParentCoordinator.childCoordinators.append(coordinator) 51 | coordinator.start() 52 | } 53 | 54 | } 55 | 56 | // MARK: - UINavigationControllerDelegate 57 | 58 | extension MainCoordinator: UINavigationControllerDelegate { 59 | 60 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 61 | guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else { 62 | return 63 | } 64 | 65 | // Check whether our view controller array already contains that view controller. 66 | // If it does it means we’re pushing a different view controller on top rather than popping it, so exit. 67 | if navigationController.viewControllers.contains(fromViewController) { 68 | return 69 | } 70 | 71 | parentCoordinator?.childDidFinish() 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/MainProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainProtocols.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/14/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol MainViewModelProtocol { 11 | 12 | func topics(for section: MainViewSection) -> [Topic] 13 | func topic(for index: Int, at section: MainViewSection) -> Topic 14 | 15 | } 16 | 17 | protocol MainCoordinatorProtocol: AnyObject { 18 | 19 | func showTopic(_ topic: Topic) 20 | 21 | } 22 | 23 | protocol MainViewFactoryProtocol { 24 | 25 | var sections: [MainViewSection] { get } 26 | 27 | func topic(for index: Int, at section: Int) -> Topic? 28 | 29 | } 30 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MainViewController: UICollectionViewController { 11 | 12 | private let factory: MainViewFactoryProtocol 13 | private var dataSource: UICollectionViewDiffableDataSource! 14 | 15 | weak var coordinator: MainCoordinatorProtocol? 16 | 17 | // MARK: - Initializers 18 | 19 | init(factory: MainViewFactoryProtocol) { 20 | self.factory = factory 21 | super.init(collectionViewLayout: UICollectionViewLayout()) 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | fatalError() 26 | } 27 | 28 | // MARK: - Lifecycle 29 | 30 | override func viewDidLoad() { 31 | super.viewDidLoad() 32 | title = "What's new in collection views?" 33 | 34 | configureCollectionView() 35 | } 36 | 37 | // MARK: - Private 38 | 39 | private func configureCollectionView() { 40 | collectionView.register(viewType: SectionHeaderView.self, kind: UICollectionView.elementKindSectionHeader) 41 | 42 | configureCollectionViewLayout() 43 | configureCollectionViewDataSource() 44 | updateUI() 45 | } 46 | 47 | private func configureCollectionViewLayout() { 48 | var config = UICollectionLayoutListConfiguration(appearance: .grouped) 49 | config.headerMode = .supplementary 50 | 51 | let layout = UICollectionViewCompositionalLayout.list(using: config) 52 | collectionView.collectionViewLayout = layout 53 | } 54 | 55 | private func configureCollectionViewDataSource() { 56 | let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 57 | var content = UIListContentConfiguration.sidebarCell() 58 | 59 | content.text = item.title 60 | content.secondaryText = item.subtitle 61 | content.textToSecondaryTextVerticalPadding = 4 62 | content.secondaryTextProperties.numberOfLines = 0 63 | 64 | cell.contentConfiguration = content 65 | } 66 | 67 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 68 | (collectionView: UICollectionView, indexPath: IndexPath, identifier: Topic) -> UICollectionViewCell? in 69 | let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, 70 | for: indexPath, item: identifier) 71 | cell.accessories = [.disclosureIndicator()] 72 | return cell 73 | } 74 | 75 | dataSource.supplementaryViewProvider = { (collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in 76 | let headerView = collectionView.dequeueReusableView(with: SectionHeaderView.self, 77 | kind: UICollectionView.elementKindSectionHeader, 78 | for: indexPath) 79 | headerView.titleLabel.text = self.factory.sections[indexPath.section].title 80 | return headerView 81 | } 82 | } 83 | 84 | private func updateUI() { 85 | let sections = factory.sections 86 | var snapshot = NSDiffableDataSourceSnapshot() 87 | snapshot.appendSections(sections) 88 | sections.forEach { snapshot.appendItems($0.topics, toSection: $0) } 89 | dataSource?.apply(snapshot, animatingDifferences: false) 90 | } 91 | 92 | // MARK: - UICollectionViewDelegate 93 | 94 | override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 95 | collectionView.deselectItem(at: indexPath, animated: true) 96 | 97 | guard let topic = factory.topic(for: indexPath.row, at: indexPath.section) else { 98 | return 99 | } 100 | coordinator?.showTopic(topic) 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/MainViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewFactory.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/14/20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MainViewFactory: MainViewFactoryProtocol { 11 | 12 | var sections: [MainViewSection] { 13 | return [.iOS13, .iOS14] 14 | } 15 | 16 | func topic(for index: Int, at section: Int) -> Topic? { 17 | guard sections.count > section else { return nil } 18 | 19 | let section = sections[section] 20 | guard section.topics.count > index else { return nil } 21 | 22 | return section.topics[index] 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/MainViewSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewSection.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | enum MainViewSection { 11 | case iOS13 12 | case iOS14 13 | 14 | var title: String? { 15 | switch self { 16 | case .iOS13: 17 | return "iOS 13" 18 | case .iOS14: 19 | return "iOS 14" 20 | } 21 | } 22 | 23 | var topics: [Topic] { 24 | switch self { 25 | case .iOS13: 26 | return [.compositionalLayouts, .diffableDataSources] 27 | case .iOS14: 28 | return [.collectionViewLists, .cellRegistration, .multipleLayouts, .otherCapabilities] 29 | } 30 | } 31 | } 32 | 33 | enum Topic { 34 | 35 | case compositionalLayouts 36 | case diffableDataSources 37 | case collectionViewLists 38 | case cellRegistration 39 | case multipleLayouts 40 | case otherCapabilities 41 | 42 | var title: String? { 43 | switch self { 44 | case .compositionalLayouts: 45 | return "UICollectionViewCompositionalLayout" 46 | case .diffableDataSources: 47 | return "UICollectionViewDiffableDataSource" 48 | case .collectionViewLists: 49 | return "Lists with collection views" 50 | case .cellRegistration: 51 | return "Cell registration" 52 | case .multipleLayouts: 53 | return "Multiple layouts" 54 | case .otherCapabilities: 55 | return "Other capabilities" 56 | } 57 | } 58 | 59 | var subtitle: String? { 60 | switch self { 61 | case .compositionalLayouts: 62 | return "Declarative collection view layouts" 63 | case .diffableDataSources: 64 | return "Collection view update's management" 65 | case .collectionViewLists: 66 | return "List cell, Content config and Layout list config" 67 | case .cellRegistration, .multipleLayouts: 68 | return nil 69 | case .otherCapabilities: 70 | return "Drag and drop and section snapshot" 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/Main/ViewComponents/SectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SectionHeaderView.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | class SectionHeaderView: UICollectionReusableView { 11 | 12 | lazy var titleLabel: UILabel = { 13 | let label = UILabel() 14 | label.textColor = .systemGray 15 | label.font = .preferredFont(forTextStyle: .footnote) 16 | 17 | label.translatesAutoresizingMaskIntoConstraints = false 18 | 19 | return label 20 | }() 21 | 22 | // MARK: - Initializers 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | setupUI() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | super.init(coder: coder) 31 | setupUI() 32 | } 33 | 34 | // MARK: - Private 35 | 36 | private func setupUI() { 37 | addSubview(titleLabel) 38 | 39 | NSLayoutConstraint.activate([ 40 | titleLabel.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 16), 41 | titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 42 | titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), 43 | titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8) 44 | ]) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/MultipleLayouts/MultipleLayoutsCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleLayoutsCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/22/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol MultipleLayoutsCoordinatorProtocol: AnyObject {} 11 | 12 | final class MultipleLayoutsCoordinator: Coordinator, MultipleLayoutsCoordinatorProtocol { 13 | 14 | var childCoordinators: [Coordinator] = [] 15 | var parentCoordinator: Coordinator? 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let factory = MultipleLayoutsViewFactory() 24 | let viewController = MultipleLayoutsViewController(factory: factory) 25 | 26 | viewController.coordinator = self 27 | 28 | navigationController.pushViewController(viewController, animated: true) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/MultipleLayouts/MultipleLayoutsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleLayoutsViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/22/21. 6 | // 7 | 8 | import UIKit 9 | 10 | struct MultipleItem: Hashable { 11 | 12 | private let identifier: UUID = UUID() 13 | let value: Int 14 | 15 | } 16 | 17 | class MultipleLayoutsViewController: UICollectionViewController { 18 | 19 | var sections: [SectionItem] = [SectionItem(type: .orthogonal), SectionItem(type: .list), SectionItem(type: .list)] 20 | 21 | // MARK: - Properties 22 | 23 | private var dataSource: UICollectionViewDiffableDataSource! 24 | 25 | private var factory: MultipleLayoutsFactoryProtocol 26 | weak var coordinator: MultipleLayoutsCoordinatorProtocol? 27 | 28 | // MARK: - Initializers 29 | 30 | init(factory: MultipleLayoutsFactoryProtocol) { 31 | self.factory = factory 32 | super.init(collectionViewLayout: UICollectionViewLayout()) 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | // MARK: - Lifecycle 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | collectionView.backgroundColor = .systemBackground 44 | 45 | setupNavigationBar() 46 | setupCollectionView() 47 | } 48 | 49 | // MARK: - Private 50 | 51 | private func setupNavigationBar() { 52 | title = "Multiple layouts" 53 | } 54 | 55 | private func setupCollectionView() { 56 | collectionView.collectionViewLayout = makeCollectionViewLayout() 57 | 58 | configureDataSource(with: .valueCell()) 59 | updateUI() 60 | } 61 | 62 | private func makeCollectionViewLayout() -> UICollectionViewLayout { 63 | return UICollectionViewCompositionalLayout { sectionNumber, env -> NSCollectionLayoutSection? in 64 | switch self.sections[sectionNumber].type { 65 | case .orthogonal: 66 | return self.factory.gridSection() 67 | case .list: 68 | return self.factory.groupedListSection(env) 69 | } 70 | } 71 | } 72 | 73 | private func configureDataSource(with contentConfiguration: UIListContentConfiguration) { 74 | 75 | // Cell Registration 76 | 77 | let numberedCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 78 | cell.number = item.value 79 | cell.backgroundColor = .cyan 80 | } 81 | 82 | let listCellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 83 | 84 | var contentConfiguration = contentConfiguration 85 | contentConfiguration.text = "Row \(item.value)" 86 | contentConfiguration.secondaryText = "Value" 87 | contentConfiguration.image = UIImage(systemName: "applelogo") 88 | 89 | cell.accessories = [ 90 | .disclosureIndicator(), 91 | .insert(displayed: .whenEditing), 92 | .delete(displayed: .whenEditing)] 93 | 94 | cell.contentConfiguration = contentConfiguration 95 | } 96 | 97 | // Diffable Data Source 98 | 99 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 100 | (collectionView, indexPath, identifier) -> UICollectionViewCell? in 101 | switch self.sections[indexPath.section].type { 102 | case .orthogonal: 103 | return collectionView.dequeueConfiguredReusableCell( 104 | using: numberedCellRegistration, 105 | for: indexPath, 106 | item: identifier) 107 | case .list: 108 | return collectionView.dequeueConfiguredReusableCell(using: listCellRegistration, for: indexPath, item: identifier) 109 | } 110 | } 111 | 112 | } 113 | 114 | private func updateUI(animated: Bool = false) { 115 | var snapshot = NSDiffableDataSourceSnapshot() 116 | snapshot.appendSections(sections) 117 | for section in sections { 118 | let items = Array(0..<15).map { MultipleItem(value: $0) } 119 | snapshot.appendItems(items, toSection: section) 120 | } 121 | dataSource?.apply(snapshot, animatingDifferences: animated) 122 | } 123 | 124 | } 125 | 126 | // MARK: - Sections 127 | 128 | extension MultipleLayoutsViewController { 129 | 130 | struct SectionItem: Hashable { 131 | private let uuid = UUID() 132 | let type: SectionType 133 | } 134 | 135 | enum SectionType: Hashable { 136 | case orthogonal 137 | case list 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/MultipleLayouts/MultipleLayoutsViewFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleLayoutsFactory.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/22/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol MultipleLayoutsFactoryProtocol { 11 | 12 | func gridSection() -> NSCollectionLayoutSection 13 | 14 | func groupedListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 15 | func insetGroupedListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 16 | func sidebarListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection 17 | 18 | } 19 | 20 | struct MultipleLayoutsViewFactory: MultipleLayoutsFactoryProtocol { 21 | 22 | func gridSection() -> NSCollectionLayoutSection { 23 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), 24 | heightDimension: .fractionalHeight(1.0)) 25 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 26 | item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) 27 | 28 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 29 | heightDimension: .fractionalHeight(0.3)) 30 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 31 | subitem: item, count: 3) 32 | 33 | let section = NSCollectionLayoutSection(group: group) 34 | section.orthogonalScrollingBehavior = .continuous 35 | 36 | return section 37 | } 38 | 39 | func insetGroupedListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { 40 | var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 41 | 42 | config.trailingSwipeActionsConfigurationProvider = { indexPath in 43 | let actionHandler: UIContextualAction.Handler = { action, view, completion in 44 | completion(true) 45 | } 46 | 47 | let checkAction = UIContextualAction(style: .normal, title: "Check", handler: actionHandler) 48 | checkAction.image = UIImage(systemName: "checkmark") 49 | checkAction.backgroundColor = .systemGreen 50 | 51 | let editAction = UIContextualAction(style: .normal, title: "Edit") { _, _, completion in 52 | completion(true) 53 | } 54 | editAction.image = UIImage(systemName: "pencil.circle") 55 | 56 | return UISwipeActionsConfiguration(actions: [checkAction, editAction]) 57 | } 58 | 59 | return NSCollectionLayoutSection.list(using: config, layoutEnvironment: env) 60 | } 61 | 62 | func groupedListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { 63 | let config = UICollectionLayoutListConfiguration(appearance: .grouped) 64 | return NSCollectionLayoutSection.list(using: config, layoutEnvironment: env) 65 | } 66 | 67 | func sidebarListSection(_ env: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection { 68 | let config = UICollectionLayoutListConfiguration(appearance: .sidebarPlain) 69 | 70 | return NSCollectionLayoutSection.list(using: config, layoutEnvironment: env) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/OtherCapabilities/OtherCapabilitiesCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherCapabilitiesCoordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol OtherCapabilitiesCoordinatorProtocol: Coordinator {} 11 | 12 | final class OtherCapabilitiesCoordinator: OtherCapabilitiesCoordinatorProtocol { 13 | 14 | var childCoordinators: [Coordinator] = [] 15 | var parentCoordinator: Coordinator? 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let viewController = OtherCapabilitiesViewController() 24 | 25 | viewController.coordinator = self 26 | 27 | navigationController.pushViewController(viewController, animated: true) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/OtherCapabilities/OtherCapabilitiesItemModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherCapabilitiesItemModel.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/22/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RowModel: Hashable { 11 | 12 | let identifier = UUID() 13 | let value: Int 14 | 15 | } 16 | 17 | struct SectionModel: Hashable { 18 | 19 | let identifier = UUID() 20 | let value: Int 21 | 22 | } 23 | 24 | enum Item: Hashable { 25 | 26 | case sectionItem(model: SectionModel) 27 | case rowItem(model: RowModel) 28 | 29 | var value: Int { 30 | switch self { 31 | case .sectionItem(let model): 32 | return model.value 33 | case .rowItem(let model): 34 | return model.value 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ModernCollectionViews/Scenes/OtherCapabilities/OtherCapabilitiesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherCapabilitiesViewController.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 2/21/21. 6 | // 7 | 8 | import UIKit 9 | 10 | class OtherCapabilitiesViewController: UICollectionViewController { 11 | 12 | // MARK: - Properties 13 | 14 | private var dataSource: UICollectionViewDiffableDataSource! 15 | 16 | private var mainItems: [Item] = Array(0..<5).map { .rowItem(model: RowModel(value: $0)) } 17 | private var secondaryItems: [Item] = Array(0..<5).map { .rowItem(model: RowModel(value: $0)) } 18 | private var tertiaryItems: [Item] = Array(0..<5).map { .rowItem(model: RowModel(value: $0)) } 19 | 20 | weak var coordinator: OtherCapabilitiesCoordinatorProtocol? 21 | 22 | // MARK: - Initializers 23 | 24 | init() { 25 | super.init(collectionViewLayout: UICollectionViewLayout()) 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | super.init(coder: coder) 30 | } 31 | 32 | // MARK: - Lifecycle 33 | 34 | override func viewDidLoad() { 35 | super.viewDidLoad() 36 | collectionView.backgroundColor = .systemBackground 37 | 38 | setupNavigationBar() 39 | setupCollectionView() 40 | } 41 | 42 | // MARK: - Private 43 | 44 | private func setupNavigationBar() { 45 | title = "Other capabilities" 46 | navigationItem.rightBarButtonItem = editButtonItem 47 | } 48 | 49 | private func setupCollectionView() { 50 | collectionView.collectionViewLayout = makeCollectionViewLayout() 51 | 52 | configureDataSource() 53 | updateUI() 54 | } 55 | 56 | private func makeCollectionViewLayout() -> UICollectionViewLayout { 57 | var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) 58 | 59 | config.trailingSwipeActionsConfigurationProvider = { indexPath in 60 | let actionHandler: UIContextualAction.Handler = { action, view, completion in 61 | completion(true) 62 | } 63 | 64 | let action = UIContextualAction(style: .normal, title: "Test", handler: actionHandler) 65 | action.image = UIImage(systemName: "checkmark") 66 | action.backgroundColor = .systemGreen 67 | 68 | return UISwipeActionsConfiguration(actions: [action]) 69 | } 70 | 71 | return UICollectionViewCompositionalLayout.list(using: config) 72 | } 73 | 74 | private func configureDataSource() { 75 | 76 | // Header Registration 77 | 78 | let headerRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 79 | var content = cell.defaultContentConfiguration() 80 | content.text = "Section \(item.value)" 81 | cell.contentConfiguration = content 82 | 83 | cell.accessories = [ 84 | .outlineDisclosure(displayed: .always, options: .init(style: .header)) 85 | ] 86 | } 87 | 88 | // Cell Registration 89 | 90 | let cellRegistration = UICollectionView.CellRegistration { cell, indexPath, item in 91 | 92 | var contentConfiguration = UIListContentConfiguration.valueCell() 93 | contentConfiguration.text = "Row \(item.value)" 94 | contentConfiguration.secondaryText = "Value" 95 | contentConfiguration.image = UIImage(systemName: "applelogo") 96 | 97 | cell.accessories = [ 98 | .disclosureIndicator(displayed: .whenNotEditing), 99 | .reorder(displayed: .whenEditing, options: .init( showsVerticalSeparator: true)) 100 | ] 101 | 102 | cell.contentConfiguration = contentConfiguration 103 | } 104 | 105 | // Diffable Data Source 106 | 107 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { 108 | (collectionView, indexPath, item) -> UICollectionViewCell? in 109 | switch item { 110 | case .sectionItem: 111 | return collectionView.dequeueConfiguredReusableCell(using: headerRegistration, 112 | for: indexPath, 113 | item: item) 114 | case .rowItem: 115 | return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, 116 | for: indexPath, 117 | item: item) 118 | } 119 | } 120 | 121 | // Reordering 122 | 123 | dataSource.reorderingHandlers.canReorderItem = { [weak self] _ in 124 | guard let self = self else { return false } 125 | return self.collectionView.isEditing 126 | } 127 | dataSource.reorderingHandlers.didReorder = { [weak self] transaction in 128 | guard let self = self else { return } 129 | for sectionTransaction in transaction.sectionTransactions { 130 | let sectionIdentifier = sectionTransaction.sectionIdentifier 131 | switch sectionIdentifier { 132 | case .main: 133 | self.mainItems = sectionTransaction.finalSnapshot.items 134 | case .secondary: 135 | self.secondaryItems = sectionTransaction.finalSnapshot.items 136 | case .tertiary: 137 | self.tertiaryItems = sectionTransaction.finalSnapshot.items 138 | } 139 | } 140 | } 141 | 142 | } 143 | 144 | private func updateUI(animated: Bool = false) { 145 | let sections = Section.allCases 146 | var snapshot = NSDiffableDataSourceSnapshot() 147 | snapshot.appendSections(sections) 148 | dataSource?.apply(snapshot, animatingDifferences: animated) 149 | 150 | for section in sections { 151 | var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() 152 | let headerItem: Item = .sectionItem(model: .init(value: section.rawValue)) 153 | sectionSnapshot.append([headerItem]) 154 | 155 | switch section { 156 | case .main: sectionSnapshot.append(mainItems, to: headerItem) 157 | case .secondary: sectionSnapshot.append(secondaryItems, to: headerItem) 158 | case .tertiary: sectionSnapshot.append(tertiaryItems, to: headerItem) 159 | } 160 | 161 | sectionSnapshot.expand([headerItem]) 162 | dataSource.apply(sectionSnapshot, to: section) 163 | } 164 | } 165 | 166 | } 167 | 168 | // MARK: - Sections 169 | 170 | extension OtherCapabilitiesViewController { 171 | 172 | enum Section: Int, CaseIterable { 173 | case main 174 | case secondary 175 | case tertiary 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /ModernCollectionViews/Shared/Extensions/Array+Filtering.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Filtering.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso Yoshio Alvarez Tengan on 10/5/20. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Array where Iterator.Element: BinaryInteger { 11 | 12 | var oddNumbers: Self { 13 | return filter { $0 % 2 != 0 } 14 | } 15 | 16 | var evenNumbers: Self { 17 | return filter { $0 % 2 == 0 } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ModernCollectionViews/Shared/Extensions/UICollectionView+Dequeuing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Dequeuing.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionView { 11 | 12 | // MARK: - Cell Register & Dequeuing 13 | 14 | func register(cellType: T.Type) { 15 | let identifier = cellType.dequeuIdentifier 16 | register(cellType, forCellWithReuseIdentifier: identifier) 17 | } 18 | 19 | func dequeueReusableCell(with type: T.Type, for indexPath: IndexPath) -> T { 20 | return self.dequeueReusableCell(withReuseIdentifier: type.dequeuIdentifier, for: indexPath) as! T 21 | } 22 | 23 | // MARK: - Reusable View Register & Dequeuing 24 | 25 | func register(viewType: T.Type, kind: String) { 26 | register(viewType, forSupplementaryViewOfKind: kind, withReuseIdentifier: kind) 27 | } 28 | 29 | func dequeueReusableView(with type: T.Type, 30 | kind: String, 31 | for indexPath: IndexPath) -> T { 32 | return dequeueReusableSupplementaryView(ofKind: kind, 33 | withReuseIdentifier: kind, 34 | for: indexPath) as! T 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /ModernCollectionViews/Shared/Protocols/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/8/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Coordinator: AnyObject { 11 | 12 | var childCoordinators: [Coordinator] { get set } 13 | var parentCoordinator: Coordinator? { get set } 14 | var navigationController: UINavigationController { get set } 15 | 16 | func start() 17 | func childDidFinish(_ child: Coordinator) 18 | 19 | } 20 | 21 | extension Coordinator { 22 | 23 | /// If we don't have a parent coordinator set up, the parent coordinator is the coordinator itself. 24 | var unwrappedParentCoordinator: Coordinator { 25 | return parentCoordinator ?? self 26 | } 27 | 28 | func childDidFinish(_ child: Coordinator) { 29 | for (index, coordinator) in childCoordinators.enumerated() where coordinator === child { 30 | childCoordinators.remove(at: index) 31 | break 32 | } 33 | } 34 | 35 | func childDidFinish() { 36 | childCoordinators.removeLast() 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ModernCollectionViews/Shared/Protocols/Dequeuable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dequeuable.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Dequeuable { 11 | 12 | static var dequeuIdentifier: String { get } 13 | 14 | } 15 | 16 | extension Dequeuable where Self: UIView { 17 | 18 | static var dequeuIdentifier: String { 19 | return String(describing: self) 20 | } 21 | 22 | } 23 | 24 | extension UICollectionViewCell: Dequeuable { } 25 | -------------------------------------------------------------------------------- /ModernCollectionViews/Shared/ViewComponents/NumberedCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberedCollectionViewCell.swift 3 | // ModernCollectionViews 4 | // 5 | // Created by Alonso on 10/4/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class NumberedCollectionViewCell: UICollectionViewCell { 11 | 12 | private lazy var numberLabel: UILabel = { 13 | let label = UILabel() 14 | label.translatesAutoresizingMaskIntoConstraints = false 15 | label.textColor = .black 16 | label.textAlignment = .center 17 | 18 | let scaledFont = UIFont.systemFont(ofSize: 25, weight: .medium) 19 | label.font = UIFontMetrics.default.scaledFont(for: scaledFont) 20 | 21 | return label 22 | }() 23 | 24 | var number: Int = 0 { 25 | didSet { 26 | numberLabel.text = "\(number)" 27 | } 28 | } 29 | 30 | // MARK: - Initializers 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | setupUI() 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | super.init(coder: coder) 39 | setupUI() 40 | } 41 | 42 | // MARK: - Private 43 | 44 | private func setupUI() { 45 | backgroundColor = .lightGray 46 | 47 | layer.cornerRadius = 10 48 | 49 | setupLabels() 50 | } 51 | 52 | private func setupLabels() { 53 | addSubview(numberLabel) 54 | NSLayoutConstraint.activate([ 55 | numberLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), 56 | numberLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 57 | numberLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), 58 | numberLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16) 59 | ]) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /ModernCollectionViewsTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ModernCollectionViewsTests/ModernCollectionViewsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModernCollectionViewsTests.swift 3 | // ModernCollectionViewsTests 4 | // 5 | // Created by Alonso on 10/6/20. 6 | // 7 | 8 | import XCTest 9 | @testable import ModernCollectionViews 10 | 11 | class ModernCollectionViewsTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /ModernCollectionViewsUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ModernCollectionViewsUITests/ModernCollectionViewsUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModernCollectionViewsUITests.swift 3 | // ModernCollectionViewsUITests 4 | // 5 | // Created by Alonso on 10/6/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class ModernCollectionViewsUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModernCollectionViews 2 | 3 | [![Platform](https://img.shields.io/badge/platform-iOS-yellow.svg)]() 4 | [![Swift 5](https://img.shields.io/badge/Swift-5-orange.svg?style=flat)](https://developer.apple.com/swift/) 5 | 6 | Sample project that demonstrates what's new in collection views for iOS 13/14. 7 | 8 | ## Requirements 9 | 10 | - iOS 14.0+ 11 | - Xcode 12+ 12 | 13 | ## Screenshots 14 | 15 | 16 | 17 | 18 | ## Contributing 19 | 20 | Feel free to open an issue or submit a pull request if you have any improvement or feedback. 21 | 22 | ## Author 23 | 24 | Alonso Alvarez, alonso.alvarez.dev@gmail.com 25 | -------------------------------------------------------------------------------- /Screenshots/Compositional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeluxeAlonso/ModernCollectionViews/fbf005f49864630e67028c71b405ad6fa6be4de4/Screenshots/Compositional.png -------------------------------------------------------------------------------- /Screenshots/Diffable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeluxeAlonso/ModernCollectionViews/fbf005f49864630e67028c71b405ad6fa6be4de4/Screenshots/Diffable.png -------------------------------------------------------------------------------- /Screenshots/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeluxeAlonso/ModernCollectionViews/fbf005f49864630e67028c71b405ad6fa6be4de4/Screenshots/Home.png -------------------------------------------------------------------------------- /Screenshots/Lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeluxeAlonso/ModernCollectionViews/fbf005f49864630e67028c71b405ad6fa6be4de4/Screenshots/Lists.png --------------------------------------------------------------------------------