├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Example └── CompositionalListExample │ ├── CompositionalListExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ ├── CompositionalListExample │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── CompositionalListExampleApp.swift │ ├── FeedContainerView.swift │ ├── FeedView.swift │ ├── Helpers │ │ ├── ActivityIndicator.swift │ │ ├── CustomLayout.swift │ │ ├── Itunes │ │ │ ├── Models │ │ │ │ └── Feed │ │ │ │ │ ├── Feed.swift │ │ │ │ │ └── FeedItem.swift │ │ │ ├── Networking │ │ │ │ ├── CombineAPI.swift │ │ │ │ ├── FeedGenerator.swift │ │ │ │ ├── ItunesClient.swift │ │ │ │ └── ItunesRemote.swift │ │ │ └── Views │ │ │ │ ├── ArtWork.swift │ │ │ │ └── ItunesFeedItemDetailView.swift │ │ └── Marvel │ │ │ ├── CarachterArtworkView.swift │ │ │ └── MarvelProvider.swift │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── TabBar.swift │ ├── CompositionalListExampleTests │ ├── CompositionalListExampleTests.swift │ └── Info.plist │ └── CompositionalListExampleUITests │ ├── CompositionalListExampleUITests.swift │ └── Info.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CompositionalList │ ├── SwiftUI │ └── CompositionalList.swift │ └── UIKit │ ├── BaseCollectionViewCell.swift │ ├── CollectionReusable.swift │ ├── DiffCollectionView.swift │ ├── HostView.swift │ ├── WrapperCollectionReusableView.swift │ └── WrapperViewCell.swift └── Tests ├── CompositionalListTests ├── CompositionalListTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 7B0128F825B65F1F0040B70B /* CustomLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0128F725B65F1F0040B70B /* CustomLayout.swift */; }; 11 | 7B0997D225AEA7B4004E3DEE /* FeedGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997D125AEA7B4004E3DEE /* FeedGenerator.swift */; }; 12 | 7B0997DA25AEB7B9004E3DEE /* CombineAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997D925AEB7B9004E3DEE /* CombineAPI.swift */; }; 13 | 7B0997DF25AEB8AC004E3DEE /* ItunesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997DE25AEB8AC004E3DEE /* ItunesClient.swift */; }; 14 | 7B0997E425AEBBA2004E3DEE /* FeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997E325AEBBA2004E3DEE /* FeedItem.swift */; }; 15 | 7B0997EC25AEBC92004E3DEE /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997EB25AEBC92004E3DEE /* Feed.swift */; }; 16 | 7B0997F425AEC20D004E3DEE /* ItunesRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997F325AEC20D004E3DEE /* ItunesRemote.swift */; }; 17 | 7B0997FA25AEC7C2004E3DEE /* ArtWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0997F925AEC7C2004E3DEE /* ArtWork.swift */; }; 18 | 7B0AB28025B8F0100079F06E /* ItunesFeedItemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0AB27F25B8F0100079F06E /* ItunesFeedItemDetailView.swift */; }; 19 | 7B1DD11625ACD0CB00897A98 /* CompositionalListExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD11525ACD0CB00897A98 /* CompositionalListExampleApp.swift */; }; 20 | 7B1DD11825ACD0CB00897A98 /* TabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD11725ACD0CB00897A98 /* TabBar.swift */; }; 21 | 7B1DD11A25ACD0CD00897A98 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B1DD11925ACD0CD00897A98 /* Assets.xcassets */; }; 22 | 7B1DD11D25ACD0CD00897A98 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B1DD11C25ACD0CD00897A98 /* Preview Assets.xcassets */; }; 23 | 7B1DD12825ACD0CD00897A98 /* CompositionalListExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD12725ACD0CD00897A98 /* CompositionalListExampleTests.swift */; }; 24 | 7B1DD13325ACD0CD00897A98 /* CompositionalListExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD13225ACD0CD00897A98 /* CompositionalListExampleUITests.swift */; }; 25 | 7B1DD14525AD1AE500897A98 /* CompositionalList in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1DD14425AD1AE500897A98 /* CompositionalList */; }; 26 | 7B1DD1D225AD557300897A98 /* MarvelClient in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1DD1D125AD557300897A98 /* MarvelClient */; }; 27 | 7B1DD1E025AD5CF600897A98 /* MarvelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD1DF25AD5CF600897A98 /* MarvelProvider.swift */; }; 28 | 7B1DD1E825AD5E5700897A98 /* CarachterArtworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD1E725AD5E5700897A98 /* CarachterArtworkView.swift */; }; 29 | 7B1DD1EE25AD5E9400897A98 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7B1DD1ED25AD5E9400897A98 /* SDWebImageSwiftUI */; }; 30 | 7B1DD1F325AD62D600897A98 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1DD1F225AD62D600897A98 /* ActivityIndicator.swift */; }; 31 | 7BF7CFF925BB474600BD3B78 /* FeedContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF7CFF825BB474600BD3B78 /* FeedContainerView.swift */; }; 32 | 7BF7D00125BB476500BD3B78 /* FeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF7D00025BB476500BD3B78 /* FeedView.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | 7B1DD12425ACD0CD00897A98 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 7B1DD10A25ACD0CB00897A98 /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = 7B1DD11125ACD0CB00897A98; 41 | remoteInfo = CompositionalListExample; 42 | }; 43 | 7B1DD12F25ACD0CD00897A98 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 7B1DD10A25ACD0CB00897A98 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 7B1DD11125ACD0CB00897A98; 48 | remoteInfo = CompositionalListExample; 49 | }; 50 | /* End PBXContainerItemProxy section */ 51 | 52 | /* Begin PBXFileReference section */ 53 | 7B0128F725B65F1F0040B70B /* CustomLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayout.swift; sourceTree = ""; }; 54 | 7B0997D125AEA7B4004E3DEE /* FeedGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedGenerator.swift; sourceTree = ""; }; 55 | 7B0997D925AEB7B9004E3DEE /* CombineAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineAPI.swift; sourceTree = ""; }; 56 | 7B0997DE25AEB8AC004E3DEE /* ItunesClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItunesClient.swift; sourceTree = ""; }; 57 | 7B0997E325AEBBA2004E3DEE /* FeedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItem.swift; sourceTree = ""; }; 58 | 7B0997EB25AEBC92004E3DEE /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 59 | 7B0997F325AEC20D004E3DEE /* ItunesRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItunesRemote.swift; sourceTree = ""; }; 60 | 7B0997F925AEC7C2004E3DEE /* ArtWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtWork.swift; sourceTree = ""; }; 61 | 7B0AB27F25B8F0100079F06E /* ItunesFeedItemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItunesFeedItemDetailView.swift; sourceTree = ""; }; 62 | 7B1DD11225ACD0CB00897A98 /* CompositionalListExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CompositionalListExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 63 | 7B1DD11525ACD0CB00897A98 /* CompositionalListExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalListExampleApp.swift; sourceTree = ""; }; 64 | 7B1DD11725ACD0CB00897A98 /* TabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBar.swift; sourceTree = ""; }; 65 | 7B1DD11925ACD0CD00897A98 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 66 | 7B1DD11C25ACD0CD00897A98 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 67 | 7B1DD11E25ACD0CD00897A98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68 | 7B1DD12325ACD0CD00897A98 /* CompositionalListExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CompositionalListExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 69 | 7B1DD12725ACD0CD00897A98 /* CompositionalListExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalListExampleTests.swift; sourceTree = ""; }; 70 | 7B1DD12925ACD0CD00897A98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 71 | 7B1DD12E25ACD0CD00897A98 /* CompositionalListExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CompositionalListExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 72 | 7B1DD13225ACD0CD00897A98 /* CompositionalListExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalListExampleUITests.swift; sourceTree = ""; }; 73 | 7B1DD13425ACD0CD00897A98 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 74 | 7B1DD1DF25AD5CF600897A98 /* MarvelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarvelProvider.swift; sourceTree = ""; }; 75 | 7B1DD1E725AD5E5700897A98 /* CarachterArtworkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarachterArtworkView.swift; sourceTree = ""; }; 76 | 7B1DD1F225AD62D600897A98 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 77 | 7BF7CFF825BB474600BD3B78 /* FeedContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedContainerView.swift; sourceTree = ""; }; 78 | 7BF7D00025BB476500BD3B78 /* FeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedView.swift; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 7B1DD10F25ACD0CB00897A98 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | 7B1DD1EE25AD5E9400897A98 /* SDWebImageSwiftUI in Frameworks */, 87 | 7B1DD1D225AD557300897A98 /* MarvelClient in Frameworks */, 88 | 7B1DD14525AD1AE500897A98 /* CompositionalList in Frameworks */, 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | 7B1DD12025ACD0CD00897A98 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | 7B1DD12B25ACD0CD00897A98 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | ); 104 | runOnlyForDeploymentPostprocessing = 0; 105 | }; 106 | /* End PBXFrameworksBuildPhase section */ 107 | 108 | /* Begin PBXGroup section */ 109 | 7B0128F625B65F160040B70B /* Helpers */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 7B1DD1DE25AD5CD800897A98 /* Marvel */, 113 | 7B0997CA25AEA734004E3DEE /* Itunes */, 114 | 7B1DD1F225AD62D600897A98 /* ActivityIndicator.swift */, 115 | 7B0128F725B65F1F0040B70B /* CustomLayout.swift */, 116 | ); 117 | path = Helpers; 118 | sourceTree = ""; 119 | }; 120 | 7B0997CA25AEA734004E3DEE /* Itunes */ = { 121 | isa = PBXGroup; 122 | children = ( 123 | 7B0997F825AEC7AD004E3DEE /* Views */, 124 | 7B0997CF25AEA772004E3DEE /* Models */, 125 | 7B0997CE25AEA75B004E3DEE /* Networking */, 126 | ); 127 | path = Itunes; 128 | sourceTree = ""; 129 | }; 130 | 7B0997CE25AEA75B004E3DEE /* Networking */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 7B0997D125AEA7B4004E3DEE /* FeedGenerator.swift */, 134 | 7B0997D925AEB7B9004E3DEE /* CombineAPI.swift */, 135 | 7B0997DE25AEB8AC004E3DEE /* ItunesClient.swift */, 136 | 7B0997F325AEC20D004E3DEE /* ItunesRemote.swift */, 137 | ); 138 | path = Networking; 139 | sourceTree = ""; 140 | }; 141 | 7B0997CF25AEA772004E3DEE /* Models */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 7B0997D025AEA777004E3DEE /* Feed */, 145 | ); 146 | path = Models; 147 | sourceTree = ""; 148 | }; 149 | 7B0997D025AEA777004E3DEE /* Feed */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 7B0997E325AEBBA2004E3DEE /* FeedItem.swift */, 153 | 7B0997EB25AEBC92004E3DEE /* Feed.swift */, 154 | ); 155 | path = Feed; 156 | sourceTree = ""; 157 | }; 158 | 7B0997F825AEC7AD004E3DEE /* Views */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 7B0997F925AEC7C2004E3DEE /* ArtWork.swift */, 162 | 7B0AB27F25B8F0100079F06E /* ItunesFeedItemDetailView.swift */, 163 | ); 164 | path = Views; 165 | sourceTree = ""; 166 | }; 167 | 7B1DD10925ACD0CB00897A98 = { 168 | isa = PBXGroup; 169 | children = ( 170 | 7B1DD11425ACD0CB00897A98 /* CompositionalListExample */, 171 | 7B1DD12625ACD0CD00897A98 /* CompositionalListExampleTests */, 172 | 7B1DD13125ACD0CD00897A98 /* CompositionalListExampleUITests */, 173 | 7B1DD11325ACD0CB00897A98 /* Products */, 174 | ); 175 | sourceTree = ""; 176 | }; 177 | 7B1DD11325ACD0CB00897A98 /* Products */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 7B1DD11225ACD0CB00897A98 /* CompositionalListExample.app */, 181 | 7B1DD12325ACD0CD00897A98 /* CompositionalListExampleTests.xctest */, 182 | 7B1DD12E25ACD0CD00897A98 /* CompositionalListExampleUITests.xctest */, 183 | ); 184 | name = Products; 185 | sourceTree = ""; 186 | }; 187 | 7B1DD11425ACD0CB00897A98 /* CompositionalListExample */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | 7B0128F625B65F160040B70B /* Helpers */, 191 | 7B1DD11525ACD0CB00897A98 /* CompositionalListExampleApp.swift */, 192 | 7B1DD11725ACD0CB00897A98 /* TabBar.swift */, 193 | 7BF7CFF825BB474600BD3B78 /* FeedContainerView.swift */, 194 | 7BF7D00025BB476500BD3B78 /* FeedView.swift */, 195 | 7B1DD11925ACD0CD00897A98 /* Assets.xcassets */, 196 | 7B1DD11E25ACD0CD00897A98 /* Info.plist */, 197 | 7B1DD11B25ACD0CD00897A98 /* Preview Content */, 198 | ); 199 | path = CompositionalListExample; 200 | sourceTree = ""; 201 | }; 202 | 7B1DD11B25ACD0CD00897A98 /* Preview Content */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 7B1DD11C25ACD0CD00897A98 /* Preview Assets.xcassets */, 206 | ); 207 | path = "Preview Content"; 208 | sourceTree = ""; 209 | }; 210 | 7B1DD12625ACD0CD00897A98 /* CompositionalListExampleTests */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 7B1DD12725ACD0CD00897A98 /* CompositionalListExampleTests.swift */, 214 | 7B1DD12925ACD0CD00897A98 /* Info.plist */, 215 | ); 216 | path = CompositionalListExampleTests; 217 | sourceTree = ""; 218 | }; 219 | 7B1DD13125ACD0CD00897A98 /* CompositionalListExampleUITests */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 7B1DD13225ACD0CD00897A98 /* CompositionalListExampleUITests.swift */, 223 | 7B1DD13425ACD0CD00897A98 /* Info.plist */, 224 | ); 225 | path = CompositionalListExampleUITests; 226 | sourceTree = ""; 227 | }; 228 | 7B1DD1DE25AD5CD800897A98 /* Marvel */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 7B1DD1DF25AD5CF600897A98 /* MarvelProvider.swift */, 232 | 7B1DD1E725AD5E5700897A98 /* CarachterArtworkView.swift */, 233 | ); 234 | path = Marvel; 235 | sourceTree = ""; 236 | }; 237 | /* End PBXGroup section */ 238 | 239 | /* Begin PBXNativeTarget section */ 240 | 7B1DD11125ACD0CB00897A98 /* CompositionalListExample */ = { 241 | isa = PBXNativeTarget; 242 | buildConfigurationList = 7B1DD13725ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExample" */; 243 | buildPhases = ( 244 | 7B1DD10E25ACD0CB00897A98 /* Sources */, 245 | 7B1DD10F25ACD0CB00897A98 /* Frameworks */, 246 | 7B1DD11025ACD0CB00897A98 /* Resources */, 247 | ); 248 | buildRules = ( 249 | ); 250 | dependencies = ( 251 | ); 252 | name = CompositionalListExample; 253 | packageProductDependencies = ( 254 | 7B1DD14425AD1AE500897A98 /* CompositionalList */, 255 | 7B1DD1D125AD557300897A98 /* MarvelClient */, 256 | 7B1DD1ED25AD5E9400897A98 /* SDWebImageSwiftUI */, 257 | ); 258 | productName = CompositionalListExample; 259 | productReference = 7B1DD11225ACD0CB00897A98 /* CompositionalListExample.app */; 260 | productType = "com.apple.product-type.application"; 261 | }; 262 | 7B1DD12225ACD0CD00897A98 /* CompositionalListExampleTests */ = { 263 | isa = PBXNativeTarget; 264 | buildConfigurationList = 7B1DD13A25ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExampleTests" */; 265 | buildPhases = ( 266 | 7B1DD11F25ACD0CD00897A98 /* Sources */, 267 | 7B1DD12025ACD0CD00897A98 /* Frameworks */, 268 | 7B1DD12125ACD0CD00897A98 /* Resources */, 269 | ); 270 | buildRules = ( 271 | ); 272 | dependencies = ( 273 | 7B1DD12525ACD0CD00897A98 /* PBXTargetDependency */, 274 | ); 275 | name = CompositionalListExampleTests; 276 | productName = CompositionalListExampleTests; 277 | productReference = 7B1DD12325ACD0CD00897A98 /* CompositionalListExampleTests.xctest */; 278 | productType = "com.apple.product-type.bundle.unit-test"; 279 | }; 280 | 7B1DD12D25ACD0CD00897A98 /* CompositionalListExampleUITests */ = { 281 | isa = PBXNativeTarget; 282 | buildConfigurationList = 7B1DD13D25ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExampleUITests" */; 283 | buildPhases = ( 284 | 7B1DD12A25ACD0CD00897A98 /* Sources */, 285 | 7B1DD12B25ACD0CD00897A98 /* Frameworks */, 286 | 7B1DD12C25ACD0CD00897A98 /* Resources */, 287 | ); 288 | buildRules = ( 289 | ); 290 | dependencies = ( 291 | 7B1DD13025ACD0CD00897A98 /* PBXTargetDependency */, 292 | ); 293 | name = CompositionalListExampleUITests; 294 | productName = CompositionalListExampleUITests; 295 | productReference = 7B1DD12E25ACD0CD00897A98 /* CompositionalListExampleUITests.xctest */; 296 | productType = "com.apple.product-type.bundle.ui-testing"; 297 | }; 298 | /* End PBXNativeTarget section */ 299 | 300 | /* Begin PBXProject section */ 301 | 7B1DD10A25ACD0CB00897A98 /* Project object */ = { 302 | isa = PBXProject; 303 | attributes = { 304 | LastSwiftUpdateCheck = 1220; 305 | LastUpgradeCheck = 1220; 306 | TargetAttributes = { 307 | 7B1DD11125ACD0CB00897A98 = { 308 | CreatedOnToolsVersion = 12.2; 309 | }; 310 | 7B1DD12225ACD0CD00897A98 = { 311 | CreatedOnToolsVersion = 12.2; 312 | TestTargetID = 7B1DD11125ACD0CB00897A98; 313 | }; 314 | 7B1DD12D25ACD0CD00897A98 = { 315 | CreatedOnToolsVersion = 12.2; 316 | TestTargetID = 7B1DD11125ACD0CB00897A98; 317 | }; 318 | }; 319 | }; 320 | buildConfigurationList = 7B1DD10D25ACD0CB00897A98 /* Build configuration list for PBXProject "CompositionalListExample" */; 321 | compatibilityVersion = "Xcode 9.3"; 322 | developmentRegion = en; 323 | hasScannedForEncodings = 0; 324 | knownRegions = ( 325 | en, 326 | Base, 327 | ); 328 | mainGroup = 7B1DD10925ACD0CB00897A98; 329 | packageReferences = ( 330 | 7B1DD14325AD1AE500897A98 /* XCRemoteSwiftPackageReference "CompositionalList" */, 331 | 7B1DD1D025AD557300897A98 /* XCRemoteSwiftPackageReference "MarvelClient" */, 332 | 7B1DD1EC25AD5E9400897A98 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, 333 | ); 334 | productRefGroup = 7B1DD11325ACD0CB00897A98 /* Products */; 335 | projectDirPath = ""; 336 | projectRoot = ""; 337 | targets = ( 338 | 7B1DD11125ACD0CB00897A98 /* CompositionalListExample */, 339 | 7B1DD12225ACD0CD00897A98 /* CompositionalListExampleTests */, 340 | 7B1DD12D25ACD0CD00897A98 /* CompositionalListExampleUITests */, 341 | ); 342 | }; 343 | /* End PBXProject section */ 344 | 345 | /* Begin PBXResourcesBuildPhase section */ 346 | 7B1DD11025ACD0CB00897A98 /* Resources */ = { 347 | isa = PBXResourcesBuildPhase; 348 | buildActionMask = 2147483647; 349 | files = ( 350 | 7B1DD11D25ACD0CD00897A98 /* Preview Assets.xcassets in Resources */, 351 | 7B1DD11A25ACD0CD00897A98 /* Assets.xcassets in Resources */, 352 | ); 353 | runOnlyForDeploymentPostprocessing = 0; 354 | }; 355 | 7B1DD12125ACD0CD00897A98 /* Resources */ = { 356 | isa = PBXResourcesBuildPhase; 357 | buildActionMask = 2147483647; 358 | files = ( 359 | ); 360 | runOnlyForDeploymentPostprocessing = 0; 361 | }; 362 | 7B1DD12C25ACD0CD00897A98 /* Resources */ = { 363 | isa = PBXResourcesBuildPhase; 364 | buildActionMask = 2147483647; 365 | files = ( 366 | ); 367 | runOnlyForDeploymentPostprocessing = 0; 368 | }; 369 | /* End PBXResourcesBuildPhase section */ 370 | 371 | /* Begin PBXSourcesBuildPhase section */ 372 | 7B1DD10E25ACD0CB00897A98 /* Sources */ = { 373 | isa = PBXSourcesBuildPhase; 374 | buildActionMask = 2147483647; 375 | files = ( 376 | 7B0997E425AEBBA2004E3DEE /* FeedItem.swift in Sources */, 377 | 7B0997EC25AEBC92004E3DEE /* Feed.swift in Sources */, 378 | 7B0997F425AEC20D004E3DEE /* ItunesRemote.swift in Sources */, 379 | 7BF7CFF925BB474600BD3B78 /* FeedContainerView.swift in Sources */, 380 | 7B0997DA25AEB7B9004E3DEE /* CombineAPI.swift in Sources */, 381 | 7B1DD1E025AD5CF600897A98 /* MarvelProvider.swift in Sources */, 382 | 7B0AB28025B8F0100079F06E /* ItunesFeedItemDetailView.swift in Sources */, 383 | 7B1DD1F325AD62D600897A98 /* ActivityIndicator.swift in Sources */, 384 | 7B1DD11825ACD0CB00897A98 /* TabBar.swift in Sources */, 385 | 7B0997DF25AEB8AC004E3DEE /* ItunesClient.swift in Sources */, 386 | 7BF7D00125BB476500BD3B78 /* FeedView.swift in Sources */, 387 | 7B0997D225AEA7B4004E3DEE /* FeedGenerator.swift in Sources */, 388 | 7B1DD11625ACD0CB00897A98 /* CompositionalListExampleApp.swift in Sources */, 389 | 7B0997FA25AEC7C2004E3DEE /* ArtWork.swift in Sources */, 390 | 7B0128F825B65F1F0040B70B /* CustomLayout.swift in Sources */, 391 | 7B1DD1E825AD5E5700897A98 /* CarachterArtworkView.swift in Sources */, 392 | ); 393 | runOnlyForDeploymentPostprocessing = 0; 394 | }; 395 | 7B1DD11F25ACD0CD00897A98 /* Sources */ = { 396 | isa = PBXSourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | 7B1DD12825ACD0CD00897A98 /* CompositionalListExampleTests.swift in Sources */, 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | 7B1DD12A25ACD0CD00897A98 /* Sources */ = { 404 | isa = PBXSourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | 7B1DD13325ACD0CD00897A98 /* CompositionalListExampleUITests.swift in Sources */, 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | /* End PBXSourcesBuildPhase section */ 412 | 413 | /* Begin PBXTargetDependency section */ 414 | 7B1DD12525ACD0CD00897A98 /* PBXTargetDependency */ = { 415 | isa = PBXTargetDependency; 416 | target = 7B1DD11125ACD0CB00897A98 /* CompositionalListExample */; 417 | targetProxy = 7B1DD12425ACD0CD00897A98 /* PBXContainerItemProxy */; 418 | }; 419 | 7B1DD13025ACD0CD00897A98 /* PBXTargetDependency */ = { 420 | isa = PBXTargetDependency; 421 | target = 7B1DD11125ACD0CB00897A98 /* CompositionalListExample */; 422 | targetProxy = 7B1DD12F25ACD0CD00897A98 /* PBXContainerItemProxy */; 423 | }; 424 | /* End PBXTargetDependency section */ 425 | 426 | /* Begin XCBuildConfiguration section */ 427 | 7B1DD13525ACD0CD00897A98 /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ALWAYS_SEARCH_USER_PATHS = NO; 431 | CLANG_ANALYZER_NONNULL = YES; 432 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 433 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 434 | CLANG_CXX_LIBRARY = "libc++"; 435 | CLANG_ENABLE_MODULES = YES; 436 | CLANG_ENABLE_OBJC_ARC = YES; 437 | CLANG_ENABLE_OBJC_WEAK = YES; 438 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 439 | CLANG_WARN_BOOL_CONVERSION = YES; 440 | CLANG_WARN_COMMA = YES; 441 | CLANG_WARN_CONSTANT_CONVERSION = YES; 442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 444 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INFINITE_RECURSION = YES; 448 | CLANG_WARN_INT_CONVERSION = YES; 449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 451 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 453 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 458 | CLANG_WARN_UNREACHABLE_CODE = YES; 459 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = dwarf; 462 | ENABLE_STRICT_OBJC_MSGSEND = YES; 463 | ENABLE_TESTABILITY = YES; 464 | GCC_C_LANGUAGE_STANDARD = gnu11; 465 | GCC_DYNAMIC_NO_PIC = NO; 466 | GCC_NO_COMMON_BLOCKS = YES; 467 | GCC_OPTIMIZATION_LEVEL = 0; 468 | GCC_PREPROCESSOR_DEFINITIONS = ( 469 | "DEBUG=1", 470 | "$(inherited)", 471 | ); 472 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 473 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 474 | GCC_WARN_UNDECLARED_SELECTOR = YES; 475 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 476 | GCC_WARN_UNUSED_FUNCTION = YES; 477 | GCC_WARN_UNUSED_VARIABLE = YES; 478 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 479 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 480 | MTL_FAST_MATH = YES; 481 | ONLY_ACTIVE_ARCH = YES; 482 | SDKROOT = iphoneos; 483 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 484 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 485 | }; 486 | name = Debug; 487 | }; 488 | 7B1DD13625ACD0CD00897A98 /* Release */ = { 489 | isa = XCBuildConfiguration; 490 | buildSettings = { 491 | ALWAYS_SEARCH_USER_PATHS = NO; 492 | CLANG_ANALYZER_NONNULL = YES; 493 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 494 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 495 | CLANG_CXX_LIBRARY = "libc++"; 496 | CLANG_ENABLE_MODULES = YES; 497 | CLANG_ENABLE_OBJC_ARC = YES; 498 | CLANG_ENABLE_OBJC_WEAK = YES; 499 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 500 | CLANG_WARN_BOOL_CONVERSION = YES; 501 | CLANG_WARN_COMMA = YES; 502 | CLANG_WARN_CONSTANT_CONVERSION = YES; 503 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 504 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 505 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 506 | CLANG_WARN_EMPTY_BODY = YES; 507 | CLANG_WARN_ENUM_CONVERSION = YES; 508 | CLANG_WARN_INFINITE_RECURSION = YES; 509 | CLANG_WARN_INT_CONVERSION = YES; 510 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 511 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 512 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 513 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 514 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 515 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 516 | CLANG_WARN_STRICT_PROTOTYPES = YES; 517 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 518 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 519 | CLANG_WARN_UNREACHABLE_CODE = YES; 520 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 521 | COPY_PHASE_STRIP = NO; 522 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 523 | ENABLE_NS_ASSERTIONS = NO; 524 | ENABLE_STRICT_OBJC_MSGSEND = YES; 525 | GCC_C_LANGUAGE_STANDARD = gnu11; 526 | GCC_NO_COMMON_BLOCKS = YES; 527 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 528 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 529 | GCC_WARN_UNDECLARED_SELECTOR = YES; 530 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 531 | GCC_WARN_UNUSED_FUNCTION = YES; 532 | GCC_WARN_UNUSED_VARIABLE = YES; 533 | IPHONEOS_DEPLOYMENT_TARGET = 14.2; 534 | MTL_ENABLE_DEBUG_INFO = NO; 535 | MTL_FAST_MATH = YES; 536 | SDKROOT = iphoneos; 537 | SWIFT_COMPILATION_MODE = wholemodule; 538 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 539 | VALIDATE_PRODUCT = YES; 540 | }; 541 | name = Release; 542 | }; 543 | 7B1DD13825ACD0CD00897A98 /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 547 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 548 | CODE_SIGN_STYLE = Automatic; 549 | DEVELOPMENT_ASSET_PATHS = "\"CompositionalListExample/Preview Content\""; 550 | DEVELOPMENT_TEAM = CQ45U4X9K3; 551 | ENABLE_PREVIEWS = YES; 552 | INFOPLIST_FILE = CompositionalListExample/Info.plist; 553 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 554 | LD_RUNPATH_SEARCH_PATHS = ( 555 | "$(inherited)", 556 | "@executable_path/Frameworks", 557 | ); 558 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExample; 559 | PRODUCT_NAME = "$(TARGET_NAME)"; 560 | SWIFT_VERSION = 5.0; 561 | TARGETED_DEVICE_FAMILY = "1,2"; 562 | }; 563 | name = Debug; 564 | }; 565 | 7B1DD13925ACD0CD00897A98 /* Release */ = { 566 | isa = XCBuildConfiguration; 567 | buildSettings = { 568 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 569 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 570 | CODE_SIGN_STYLE = Automatic; 571 | DEVELOPMENT_ASSET_PATHS = "\"CompositionalListExample/Preview Content\""; 572 | DEVELOPMENT_TEAM = CQ45U4X9K3; 573 | ENABLE_PREVIEWS = YES; 574 | INFOPLIST_FILE = CompositionalListExample/Info.plist; 575 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 576 | LD_RUNPATH_SEARCH_PATHS = ( 577 | "$(inherited)", 578 | "@executable_path/Frameworks", 579 | ); 580 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExample; 581 | PRODUCT_NAME = "$(TARGET_NAME)"; 582 | SWIFT_VERSION = 5.0; 583 | TARGETED_DEVICE_FAMILY = "1,2"; 584 | }; 585 | name = Release; 586 | }; 587 | 7B1DD13B25ACD0CD00897A98 /* Debug */ = { 588 | isa = XCBuildConfiguration; 589 | buildSettings = { 590 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 591 | BUNDLE_LOADER = "$(TEST_HOST)"; 592 | CODE_SIGN_STYLE = Automatic; 593 | DEVELOPMENT_TEAM = CQ45U4X9K3; 594 | INFOPLIST_FILE = CompositionalListExampleTests/Info.plist; 595 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 596 | LD_RUNPATH_SEARCH_PATHS = ( 597 | "$(inherited)", 598 | "@executable_path/Frameworks", 599 | "@loader_path/Frameworks", 600 | ); 601 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExampleTests; 602 | PRODUCT_NAME = "$(TARGET_NAME)"; 603 | SWIFT_VERSION = 5.0; 604 | TARGETED_DEVICE_FAMILY = "1,2"; 605 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CompositionalListExample.app/CompositionalListExample"; 606 | }; 607 | name = Debug; 608 | }; 609 | 7B1DD13C25ACD0CD00897A98 /* Release */ = { 610 | isa = XCBuildConfiguration; 611 | buildSettings = { 612 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 613 | BUNDLE_LOADER = "$(TEST_HOST)"; 614 | CODE_SIGN_STYLE = Automatic; 615 | DEVELOPMENT_TEAM = CQ45U4X9K3; 616 | INFOPLIST_FILE = CompositionalListExampleTests/Info.plist; 617 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 618 | LD_RUNPATH_SEARCH_PATHS = ( 619 | "$(inherited)", 620 | "@executable_path/Frameworks", 621 | "@loader_path/Frameworks", 622 | ); 623 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExampleTests; 624 | PRODUCT_NAME = "$(TARGET_NAME)"; 625 | SWIFT_VERSION = 5.0; 626 | TARGETED_DEVICE_FAMILY = "1,2"; 627 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CompositionalListExample.app/CompositionalListExample"; 628 | }; 629 | name = Release; 630 | }; 631 | 7B1DD13E25ACD0CD00897A98 /* Debug */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 635 | CODE_SIGN_STYLE = Automatic; 636 | DEVELOPMENT_TEAM = CQ45U4X9K3; 637 | INFOPLIST_FILE = CompositionalListExampleUITests/Info.plist; 638 | LD_RUNPATH_SEARCH_PATHS = ( 639 | "$(inherited)", 640 | "@executable_path/Frameworks", 641 | "@loader_path/Frameworks", 642 | ); 643 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExampleUITests; 644 | PRODUCT_NAME = "$(TARGET_NAME)"; 645 | SWIFT_VERSION = 5.0; 646 | TARGETED_DEVICE_FAMILY = "1,2"; 647 | TEST_TARGET_NAME = CompositionalListExample; 648 | }; 649 | name = Debug; 650 | }; 651 | 7B1DD13F25ACD0CD00897A98 /* Release */ = { 652 | isa = XCBuildConfiguration; 653 | buildSettings = { 654 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 655 | CODE_SIGN_STYLE = Automatic; 656 | DEVELOPMENT_TEAM = CQ45U4X9K3; 657 | INFOPLIST_FILE = CompositionalListExampleUITests/Info.plist; 658 | LD_RUNPATH_SEARCH_PATHS = ( 659 | "$(inherited)", 660 | "@executable_path/Frameworks", 661 | "@loader_path/Frameworks", 662 | ); 663 | PRODUCT_BUNDLE_IDENTIFIER = jamesrochabrun.CompositionalListExampleUITests; 664 | PRODUCT_NAME = "$(TARGET_NAME)"; 665 | SWIFT_VERSION = 5.0; 666 | TARGETED_DEVICE_FAMILY = "1,2"; 667 | TEST_TARGET_NAME = CompositionalListExample; 668 | }; 669 | name = Release; 670 | }; 671 | /* End XCBuildConfiguration section */ 672 | 673 | /* Begin XCConfigurationList section */ 674 | 7B1DD10D25ACD0CB00897A98 /* Build configuration list for PBXProject "CompositionalListExample" */ = { 675 | isa = XCConfigurationList; 676 | buildConfigurations = ( 677 | 7B1DD13525ACD0CD00897A98 /* Debug */, 678 | 7B1DD13625ACD0CD00897A98 /* Release */, 679 | ); 680 | defaultConfigurationIsVisible = 0; 681 | defaultConfigurationName = Release; 682 | }; 683 | 7B1DD13725ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExample" */ = { 684 | isa = XCConfigurationList; 685 | buildConfigurations = ( 686 | 7B1DD13825ACD0CD00897A98 /* Debug */, 687 | 7B1DD13925ACD0CD00897A98 /* Release */, 688 | ); 689 | defaultConfigurationIsVisible = 0; 690 | defaultConfigurationName = Release; 691 | }; 692 | 7B1DD13A25ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExampleTests" */ = { 693 | isa = XCConfigurationList; 694 | buildConfigurations = ( 695 | 7B1DD13B25ACD0CD00897A98 /* Debug */, 696 | 7B1DD13C25ACD0CD00897A98 /* Release */, 697 | ); 698 | defaultConfigurationIsVisible = 0; 699 | defaultConfigurationName = Release; 700 | }; 701 | 7B1DD13D25ACD0CD00897A98 /* Build configuration list for PBXNativeTarget "CompositionalListExampleUITests" */ = { 702 | isa = XCConfigurationList; 703 | buildConfigurations = ( 704 | 7B1DD13E25ACD0CD00897A98 /* Debug */, 705 | 7B1DD13F25ACD0CD00897A98 /* Release */, 706 | ); 707 | defaultConfigurationIsVisible = 0; 708 | defaultConfigurationName = Release; 709 | }; 710 | /* End XCConfigurationList section */ 711 | 712 | /* Begin XCRemoteSwiftPackageReference section */ 713 | 7B1DD14325AD1AE500897A98 /* XCRemoteSwiftPackageReference "CompositionalList" */ = { 714 | isa = XCRemoteSwiftPackageReference; 715 | repositoryURL = "https://github.com/jamesrochabrun/CompositionalList"; 716 | requirement = { 717 | kind = upToNextMajorVersion; 718 | minimumVersion = 1.0.0; 719 | }; 720 | }; 721 | 7B1DD1D025AD557300897A98 /* XCRemoteSwiftPackageReference "MarvelClient" */ = { 722 | isa = XCRemoteSwiftPackageReference; 723 | repositoryURL = "https://github.com/jamesrochabrun/MarvelClient"; 724 | requirement = { 725 | kind = upToNextMajorVersion; 726 | minimumVersion = 1.0.0; 727 | }; 728 | }; 729 | 7B1DD1EC25AD5E9400897A98 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { 730 | isa = XCRemoteSwiftPackageReference; 731 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI"; 732 | requirement = { 733 | kind = upToNextMajorVersion; 734 | minimumVersion = 1.5.0; 735 | }; 736 | }; 737 | /* End XCRemoteSwiftPackageReference section */ 738 | 739 | /* Begin XCSwiftPackageProductDependency section */ 740 | 7B1DD14425AD1AE500897A98 /* CompositionalList */ = { 741 | isa = XCSwiftPackageProductDependency; 742 | package = 7B1DD14325AD1AE500897A98 /* XCRemoteSwiftPackageReference "CompositionalList" */; 743 | productName = CompositionalList; 744 | }; 745 | 7B1DD1D125AD557300897A98 /* MarvelClient */ = { 746 | isa = XCSwiftPackageProductDependency; 747 | package = 7B1DD1D025AD557300897A98 /* XCRemoteSwiftPackageReference "MarvelClient" */; 748 | productName = MarvelClient; 749 | }; 750 | 7B1DD1ED25AD5E9400897A98 /* SDWebImageSwiftUI */ = { 751 | isa = XCSwiftPackageProductDependency; 752 | package = 7B1DD1EC25AD5E9400897A98 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; 753 | productName = SDWebImageSwiftUI; 754 | }; 755 | /* End XCSwiftPackageProductDependency section */ 756 | }; 757 | rootObject = 7B1DD10A25ACD0CB00897A98 /* Project object */; 758 | } 759 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "CompositionalList", 6 | "repositoryURL": "https://github.com/jamesrochabrun/CompositionalList", 7 | "state": { 8 | "branch": null, 9 | "revision": "d0fd87583ad102169f9eeaa84fce22bb7adc6023", 10 | "version": "1.0.4" 11 | } 12 | }, 13 | { 14 | "package": "MarvelClient", 15 | "repositoryURL": "https://github.com/jamesrochabrun/MarvelClient", 16 | "state": { 17 | "branch": null, 18 | "revision": "1a93d163eacbe82bc0d3d62dbec3e64ce036baf7", 19 | "version": "1.0.3" 20 | } 21 | }, 22 | { 23 | "package": "SDWebImage", 24 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "a72df4849408da7e5d3c1b586797b7c601c41d1b", 28 | "version": "5.12.1" 29 | } 30 | }, 31 | { 32 | "package": "SDWebImageSwiftUI", 33 | "repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI", 34 | "state": { 35 | "branch": null, 36 | "revision": "4c7f169e39bc35d6b80d42b8eb8301bee9cd0907", 37 | "version": "1.5.0" 38 | } 39 | } 40 | ] 41 | }, 42 | "version": 1 43 | } 44 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/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 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/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 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/CompositionalListExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalListExampleApp.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import SwiftUI 9 | import CompositionalList 10 | 11 | @main 12 | struct CompositionalListExampleApp: App { 13 | 14 | var body: some Scene { 15 | WindowGroup { 16 | TabBar() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/FeedContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedContainerView.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/22/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FeedContainerView: View { 11 | 12 | @StateObject private var remote = ItunesRemote() 13 | 14 | var navigationBarTitle: Text { 15 | Text(remote.feedItems.first?.cellIdentifiers.first?.kind.capitalized ?? "Loading...") 16 | } 17 | 18 | var feedKind: Itunes.ItunesMediaType 19 | 20 | var body: some View { 21 | NavigationView { 22 | FeedView(items: $remote.feedItems, selectedItem: nil) 23 | .ignoresSafeArea() 24 | .navigationBarTitle(navigationBarTitle) 25 | } 26 | .onAppear { 27 | switch feedKind { 28 | case .apps: 29 | remote.fetchItems(.apps(contentType: .apps, chart: .topFree, limit: 100, format: .json)) 30 | case .books: 31 | remote.fetchItems(.books(contentType: .books, chart: .topFree, limit: 100, format: .json)) 32 | case .podcasts: 33 | remote.fetchItems(.podcasts(contentType: .podcasts, chart: .top, limit: 100, format: .json)) 34 | case .audioBooks: 35 | remote.fetchItems(.audioBooks(contentType: .audiobooks, chart: .top, limit: 100, format: .json)) 36 | case .music: 37 | remote.fetchItems(.music(contentType: .albums, chart: .mostPlayed, limit: 100, format: .json)) 38 | } 39 | } 40 | } 41 | } 42 | 43 | struct FeedContainerView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | FeedContainerView(feedKind: .apps) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/FeedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedView.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/22/21. 6 | // 7 | 8 | import SwiftUI 9 | import CompositionalList 10 | 11 | struct FeedView: View { 12 | 13 | @Binding var items: [GenericSectionIdentifierViewModel] 14 | @State var selectedItem: FeedItemViewModel? 15 | 16 | var body: some View { 17 | if items.isEmpty { 18 | ActivityIndicator() 19 | } else { 20 | CompositionalList(items) { model, indexPath in 21 | Group { 22 | switch indexPath.section { 23 | case 0, 2, 3: 24 | TileInfo(artworkViewModel: model) 25 | case 1: 26 | ListItem(artworkViewModel: model) 27 | default: 28 | ArtWork(artworkViewModel: model) 29 | } 30 | } 31 | }.sectionHeader { sectionIdentifier, kind, indexPath in 32 | TitleHeaderView(title: sectionIdentifier.rawValue) 33 | } 34 | .selectedItem { 35 | selectedItem = $0 36 | } 37 | .customLayout(.composed()) 38 | .sheet(item: $selectedItem) { item in 39 | ItunesFeedItemDetailView(viewModel: item) 40 | } 41 | } 42 | } 43 | } 44 | 45 | struct TitleHeaderView: View { 46 | 47 | let title: String 48 | var body: some View { 49 | VStack { 50 | HStack { 51 | Text(title) 52 | .bold() 53 | .font(.title) 54 | Spacer() 55 | } 56 | Divider() 57 | } 58 | .padding() 59 | } 60 | } 61 | 62 | 63 | // 64 | //struct FeedView_Previews: PreviewProvider { 65 | // static var previews: some View { 66 | // FeedView(items: []) 67 | // } 68 | //} 69 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityIndicator.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import SwiftUI 9 | import CompositionalList 10 | 11 | struct ActivityIndicator: UIViewRepresentable { 12 | func makeUIView(context: Context) -> UIActivityIndicatorView { 13 | let indicator = UIActivityIndicatorView(style: .large) 14 | indicator.startAnimating() 15 | return indicator 16 | } 17 | 18 | func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/CustomLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomLayout.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/18/21. 6 | // 7 | 8 | import UIKit 9 | 10 | public extension UICollectionViewLayout { 11 | 12 | // Composed layout example. 13 | static func composed() -> UICollectionViewLayout { 14 | 15 | return UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in 16 | let section: NSCollectionLayoutSection 17 | switch sectionIndex { 18 | case 0: 19 | section = .horizontalTiles(scrollingBehavior: .continuousGroupLeadingBoundary, groupSize: .init(widthDimension: .fractionalWidth(0.45), heightDimension: .fractionalWidth(0.55)), header: true) 20 | case 1: 21 | section = .groupedList(header: true) 22 | case 2: 23 | section = .horizontalTiles(scrollingBehavior: .continuousGroupLeadingBoundary, groupSize: .init(widthDimension: .fractionalWidth(0.92), heightDimension: .fractionalWidth(0.95)), header: true) 24 | case 3: 25 | section = .verticalGroupTiles(scrollingBehavior: .continuousGroupLeadingBoundary, groupSize: .init(widthDimension: .fractionalWidth(0.45), heightDimension: .fractionalWidth(1.1)), header: true) 26 | default: 27 | section = .tilesSection(header: true) 28 | } 29 | return layoutEnvironment.isPortraitEnvironment ? section : NSCollectionLayoutSection.grid(5) 30 | } 31 | } 32 | } 33 | 34 | 35 | /// Define a list of layout. 36 | @available(iOS 13.0, *) 37 | public extension NSCollectionLayoutEnvironment { 38 | 39 | var isPortraitEnvironment: Bool { 40 | container.contentSize.height > container.contentSize.width 41 | } 42 | } 43 | 44 | public extension UITraitCollection { 45 | 46 | var isRegularWidthRegularHeight: Bool { 47 | horizontalSizeClass == .regular && verticalSizeClass == .regular 48 | } 49 | } 50 | 51 | // MARK:- Helper Models 52 | @available(iOS 13.0, *) 53 | public enum ScrollAxis { 54 | case vertical 55 | case horizontal(UICollectionLayoutSectionOrthogonalScrollingBehavior) 56 | } 57 | 58 | public enum FlowDirection: CaseIterable { 59 | case topLeading 60 | case topTrailing 61 | case bottomLeading 62 | case bottomTrailing 63 | } 64 | 65 | @available(iOS 13.0, *) 66 | public struct LayoutDimension { 67 | var itemWidth: CGFloat? = nil 68 | let itemInset: NSDirectionalEdgeInsets 69 | let sectionInset: NSDirectionalEdgeInsets 70 | } 71 | 72 | @available(iOS 13.0, *) 73 | public extension LayoutDimension { 74 | 75 | init(width: CGFloat) { 76 | self.itemWidth = width 77 | self.itemInset = .zero 78 | self.sectionInset = .zero 79 | } 80 | } 81 | 82 | // MARK:- NSCollectionLayoutSection 83 | @available(iOS 13.0, *) 84 | public extension NSCollectionLayoutSection { 85 | 86 | private static func supplementaryItems(header: Bool, footer: Bool) -> [NSCollectionLayoutBoundarySupplementaryItem] { 87 | let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 88 | heightDimension: .estimated(200)) // <- estimated will dynamically adjust to less height if needed. 89 | let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem( 90 | layoutSize: headerFooterSize, 91 | elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) 92 | let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem( 93 | layoutSize: headerFooterSize, 94 | elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) 95 | var supplementaryItems: [NSCollectionLayoutBoundarySupplementaryItem] = [] 96 | if header { 97 | supplementaryItems.append(sectionHeader) 98 | } 99 | if footer { 100 | supplementaryItems.append(sectionFooter) 101 | } 102 | return supplementaryItems 103 | } 104 | 105 | /// List iOS 13 106 | static func listWith(scrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior, 107 | header: Bool = false, 108 | footer: Bool = false) -> NSCollectionLayoutSection { 109 | // 2 110 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 111 | heightDimension: .fractionalHeight(1)) 112 | // 3 113 | let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) 114 | layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) 115 | 116 | // 4 117 | let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 118 | heightDimension: .estimated(250)) 119 | // 5 120 | let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, 121 | subitems: [layoutItem]) 122 | 123 | // 6 124 | let layoutSection = NSCollectionLayoutSection(group: layoutGroup) 125 | 126 | // 7 127 | layoutSection.orthogonalScrollingBehavior = scrollingBehavior 128 | 129 | layoutSection.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 130 | 131 | return layoutSection 132 | } 133 | 134 | /// Tiles 135 | static func tilesSection(itemInset: NSDirectionalEdgeInsets = .all(1.0), 136 | header: Bool = false, 137 | footer: Bool = false) -> NSCollectionLayoutSection { 138 | 139 | let groupsFraction: CGFloat = 5.0 // needs to be equal of nestedSubGroups count 140 | /// PART 1 141 | let firstGroup = NSCollectionLayoutGroup.mainContentTopLeadingWith(itemInset, fraction: groupsFraction) 142 | /// PART 2 143 | let secondGroup = NSCollectionLayoutGroup.mainContentTopTrailingWith(itemInset, fraction: groupsFraction) 144 | /// PART3 145 | let thirdGroup = NSCollectionLayoutGroup.mainContentBottomLeadingWith(itemInset, fraction: groupsFraction) 146 | 147 | let fourdGroup = NSCollectionLayoutGroup.mainContentBottomTrailingWith(itemInset, fraction: groupsFraction) 148 | 149 | let fifthGroup = NSCollectionLayoutGroup.mainContentVerticalRectangle(itemInset, fraction: groupsFraction, rectanglePosition: FlowDirection.topTrailing) 150 | 151 | /// FINAL GROUP 152 | let nestedSubGroups = [firstGroup, secondGroup, thirdGroup, fourdGroup, fifthGroup] 153 | let nestedSubGroupsCount = CGFloat(nestedSubGroups.count) 154 | 155 | let finalNestedGroup = NSCollectionLayoutGroup.vertical( 156 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 157 | heightDimension: .fractionalWidth(nestedSubGroupsCount)), 158 | subitems: nestedSubGroups) 159 | 160 | let section = NSCollectionLayoutSection(group: finalNestedGroup) 161 | section.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 162 | return section 163 | } 164 | 165 | /// Grid layout 166 | static func grid(_ columns: Int, 167 | contentInsets: NSDirectionalEdgeInsets = .all(0), 168 | sectionInsets: NSDirectionalEdgeInsets = .all(0), 169 | header: Bool = false, 170 | footer: Bool = false) -> NSCollectionLayoutSection { 171 | 172 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 173 | heightDimension: .fractionalHeight(1.0)) 174 | 175 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 176 | 177 | /// min line spacing and the min spearator 178 | item.contentInsets = contentInsets 179 | 180 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 181 | heightDimension: .fractionalWidth(1.0 / CGFloat(columns))) 182 | 183 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns) 184 | 185 | /// sections insets 186 | let section = NSCollectionLayoutSection(group: group) 187 | 188 | section.contentInsets = sectionInsets 189 | 190 | section.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 191 | 192 | return section 193 | } 194 | 195 | /// Layout with dimensions 196 | static func layoutWithDimension(dimension: LayoutDimension, 197 | scrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .continuousGroupLeadingBoundary, 198 | header: Bool = false, 199 | footer: Bool = false) -> NSCollectionLayoutSection { 200 | /// ideal for squares 201 | let width = dimension.itemWidth ?? 0 202 | 203 | let itemSize = NSCollectionLayoutSize(widthDimension: NSCollectionLayoutDimension.absolute(width), 204 | heightDimension: NSCollectionLayoutDimension.fractionalHeight(1.0)) 205 | 206 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 207 | 208 | /// min line spacing and the min spearator 209 | item.contentInsets = dimension.itemInset 210 | 211 | let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(width), 212 | heightDimension: .estimated(250)) 213 | 214 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [item]) 215 | 216 | /// sections insets 217 | let section = NSCollectionLayoutSection(group: group) 218 | 219 | section.orthogonalScrollingBehavior = scrollingBehavior 220 | 221 | section.contentInsets = dimension.sectionInset 222 | 223 | section.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 224 | 225 | return section 226 | } 227 | 228 | static func verticalGroupTiles(scrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .continuousGroupLeadingBoundary, 229 | groupSize: NSCollectionLayoutSize, 230 | header: Bool = false, 231 | footer: Bool = false) -> NSCollectionLayoutSection { 232 | 233 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 234 | heightDimension: .fractionalHeight(1)) 235 | let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) 236 | layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5, bottom: 5, trailing: 5) 237 | let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitem: layoutItem, count: 2) 238 | let layoutSection: NSCollectionLayoutSection = .init(group: group) 239 | layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary 240 | layoutSection.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 15) 241 | layoutSection.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 242 | return layoutSection 243 | } 244 | 245 | static func groupedList(rows: CGFloat = 4, 246 | itemHeight: CGFloat = 60, 247 | scrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .continuousGroupLeadingBoundary, 248 | header: Bool = false, 249 | footer: Bool = false) -> NSCollectionLayoutSection { 250 | 251 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 252 | heightDimension: .absolute(itemHeight)) 253 | 254 | let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) 255 | layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 0, bottom: 5, trailing: 0) 256 | 257 | let layoutGroup = NSCollectionLayoutGroup.vertical( 258 | layoutSize: .init(widthDimension: .fractionalWidth(0.92), 259 | heightDimension: .absolute(itemHeight * rows)), 260 | subitem: layoutItem, count: Int(rows)) 261 | 262 | let layoutSection = NSCollectionLayoutSection(group: layoutGroup) 263 | layoutSection.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 15) 264 | layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary 265 | layoutSection.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 266 | 267 | return layoutSection 268 | } 269 | 270 | static func horizontalTiles(scrollingBehavior: UICollectionLayoutSectionOrthogonalScrollingBehavior = .continuousGroupLeadingBoundary, 271 | groupSize: NSCollectionLayoutSize, 272 | header: Bool = false, 273 | footer: Bool = false) -> NSCollectionLayoutSection { 274 | 275 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 276 | heightDimension: .fractionalHeight(1)) 277 | let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize) 278 | layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 5.0, leading: 5, bottom: 5, trailing: 5) 279 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [layoutItem]) 280 | let layoutSection: NSCollectionLayoutSection = .init(group: group) 281 | layoutSection.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary 282 | layoutSection.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 15) 283 | layoutSection.boundarySupplementaryItems = supplementaryItems(header: header, footer: footer) 284 | return layoutSection 285 | } 286 | } 287 | 288 | // MARK: Groups 289 | @available(iOS 13.0, *) 290 | public extension NSCollectionLayoutGroup { 291 | 292 | /// Returns a square full width aspect ratio 1:1 293 | static func fullSquareGroupWith(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat) -> NSCollectionLayoutGroup { 294 | 295 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 296 | heightDimension: .fractionalHeight(1.0)) 297 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 298 | item.contentInsets = insets 299 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 300 | heightDimension: .fractionalHeight(CGFloat(1.0/fraction))) 301 | return NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1) 302 | } 303 | 304 | /// Returns a Grid with a main content on the leading top area, a grid of 3 items on the bottom and a grid of to items in the trailing section 305 | static func mainContentTopLeadingWith(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat) -> NSCollectionLayoutGroup { 306 | 307 | // Big leading content 308 | let mainContentLeadingItem = NSCollectionLayoutItem.mainItem 309 | mainContentLeadingItem.contentInsets = insets 310 | 311 | // 2 vertical groups of 2 items each. 312 | let vertircalRegularItem = NSCollectionLayoutItem.verticalRegularItem 313 | vertircalRegularItem.contentInsets = insets 314 | let topTrailingGroup = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItem) 315 | 316 | // Horizontal top group 317 | let nestedTopHorizontalGroup = NSCollectionLayoutGroup.nestedHorizontalGroup([mainContentLeadingItem, topTrailingGroup]) 318 | 319 | //// bottom group 320 | let bottomRegularItem = NSCollectionLayoutItem.horizontalRegularItem 321 | bottomRegularItem.contentInsets = insets 322 | 323 | // Horizontal bottom group a row of 3 items 324 | let horizontalBottomGroup = NSCollectionLayoutGroup.horizontal3RegularItems(bottomRegularItem) 325 | 326 | return NSCollectionLayoutGroup.vertical( 327 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 328 | heightDimension: .fractionalHeight(1/fraction)), 329 | subitems: [nestedTopHorizontalGroup, horizontalBottomGroup]) 330 | } 331 | 332 | static func mainContentTopTrailingWith(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat) -> NSCollectionLayoutGroup { 333 | 334 | // Big trailing content 335 | let trailingBigItem = NSCollectionLayoutItem.mainItem 336 | trailingBigItem.contentInsets = insets 337 | 338 | // Vertical Group 2 items 339 | let vertircalRegularItem = NSCollectionLayoutItem.verticalRegularItem 340 | vertircalRegularItem.contentInsets = insets 341 | let topLeadingGroup = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItem) 342 | 343 | // Horizontal group main content + vertical items 344 | let nestedTopHorizontalGroup = NSCollectionLayoutGroup.nestedHorizontalGroup([topLeadingGroup, trailingBigItem]) 345 | 346 | //// bottom Section 347 | let bottomRegularItem = NSCollectionLayoutItem.horizontalRegularItem 348 | bottomRegularItem.contentInsets = insets 349 | 350 | let horizontalBottomGroup = NSCollectionLayoutGroup.horizontal3RegularItems(bottomRegularItem) 351 | 352 | return NSCollectionLayoutGroup.vertical( 353 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 354 | heightDimension: .fractionalHeight(1/fraction)), 355 | subitems: [nestedTopHorizontalGroup, horizontalBottomGroup]) 356 | } 357 | 358 | static func mainContentBottomTrailingWith(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat) -> NSCollectionLayoutGroup { 359 | 360 | //// top group 361 | let topRegularItem = NSCollectionLayoutItem.horizontalRegularItem 362 | topRegularItem.contentInsets = insets 363 | let horizontalTopGroup = NSCollectionLayoutGroup.horizontal3RegularItems(topRegularItem) 364 | 365 | // Big trailing content 366 | let trailingBigItem = NSCollectionLayoutItem.mainItem 367 | trailingBigItem.contentInsets = insets 368 | 369 | // Vertical Group 2 items 370 | let vertircalRegularItem = NSCollectionLayoutItem.verticalRegularItem 371 | vertircalRegularItem.contentInsets = insets 372 | let topLeadingVerticalGroup = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItem) 373 | 374 | // Horizontal group main content + vertical items 375 | let nestedBottomHorizontalGroup = NSCollectionLayoutGroup.nestedHorizontalGroup([topLeadingVerticalGroup, trailingBigItem]) 376 | 377 | return NSCollectionLayoutGroup.vertical( 378 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 379 | heightDimension: .fractionalHeight(1/fraction)), 380 | subitems: [horizontalTopGroup, nestedBottomHorizontalGroup]) 381 | } 382 | 383 | static func mainContentBottomLeadingWith(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat) -> NSCollectionLayoutGroup { 384 | 385 | //// top group 386 | let topRegularItem = NSCollectionLayoutItem.horizontalRegularItem 387 | topRegularItem.contentInsets = insets 388 | let horizontalTopGroup = NSCollectionLayoutGroup.horizontal3RegularItems(topRegularItem) 389 | 390 | // Big trailing content 391 | let trailingBigItem = NSCollectionLayoutItem.mainItem 392 | trailingBigItem.contentInsets = insets 393 | 394 | // Vertical Group 2 items 395 | let vertircalRegularItem = NSCollectionLayoutItem.verticalRegularItem 396 | vertircalRegularItem.contentInsets = insets 397 | let topTrailingVerticalGroup = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItem) 398 | 399 | // Horizontal group main content + vertical items 400 | let nestedBottomHorizontalGroup = NSCollectionLayoutGroup.nestedHorizontalGroup([trailingBigItem, topTrailingVerticalGroup]) 401 | 402 | return NSCollectionLayoutGroup.vertical( 403 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 404 | heightDimension: .fractionalHeight(1/fraction)), 405 | subitems: [horizontalTopGroup, nestedBottomHorizontalGroup]) 406 | } 407 | 408 | /// Vertical rectangular as main content 409 | static func mainContentVerticalRectangle(_ insets: NSDirectionalEdgeInsets, fraction: CGFloat, rectanglePosition: FlowDirection) -> NSCollectionLayoutGroup { 410 | 411 | //// top group 412 | let rectangularVerticalitem = NSCollectionLayoutItem.verticalRectangularItem 413 | rectangularVerticalitem.contentInsets = insets 414 | 415 | // Vertical Group A items 416 | let vertircalRegularItemA = NSCollectionLayoutItem.verticalRegularItem 417 | vertircalRegularItemA.contentInsets = insets 418 | let topVertircalRegularItemA = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItemA) 419 | 420 | // Vertical Group B items 421 | let vertircalRegularItemB = NSCollectionLayoutItem.verticalRegularItem 422 | vertircalRegularItemB.contentInsets = insets 423 | let topVertircalRegularItemB = NSCollectionLayoutGroup.vertical2RegularItems(vertircalRegularItemB) 424 | 425 | // Horizontal group main content + vertical items position 426 | var nestedMainGroupItems: [NSCollectionLayoutItem] = [] 427 | switch rectanglePosition { 428 | case .bottomLeading, .topLeading: 429 | nestedMainGroupItems.append(contentsOf: [rectangularVerticalitem, topVertircalRegularItemA, topVertircalRegularItemB]) 430 | case .bottomTrailing, .topTrailing: 431 | nestedMainGroupItems.append(contentsOf: [topVertircalRegularItemA, topVertircalRegularItemB, rectangularVerticalitem]) 432 | } 433 | let nestedMainContentGroup = NSCollectionLayoutGroup.nestedHorizontalGroup(nestedMainGroupItems) 434 | 435 | /// 3 horizontal items group 436 | let horizontalRegularItem = NSCollectionLayoutItem.horizontalRegularItem 437 | horizontalRegularItem.contentInsets = insets 438 | let horizontalThreeItemsGroup = NSCollectionLayoutGroup.horizontal3RegularItems(horizontalRegularItem) 439 | 440 | /// Final Main group 441 | var group: [NSCollectionLayoutItem] = [] 442 | switch rectanglePosition { 443 | case .bottomLeading, .bottomTrailing: 444 | group.append(contentsOf: [horizontalThreeItemsGroup, nestedMainContentGroup]) 445 | case .topLeading, .topTrailing: 446 | group.append(contentsOf: [nestedMainContentGroup, horizontalThreeItemsGroup]) 447 | } 448 | 449 | return NSCollectionLayoutGroup.vertical( 450 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 451 | heightDimension: .fractionalHeight(1/fraction)), 452 | subitems: group) 453 | } 454 | } 455 | 456 | 457 | // MARK:- Single item 458 | @available(iOS 13.0, *) 459 | public extension NSCollectionLayoutItem { 460 | 461 | static var mainItem: NSCollectionLayoutItem { 462 | NSCollectionLayoutItem( 463 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(2/3), 464 | heightDimension: .fractionalHeight(1.0))) 465 | } 466 | 467 | static var verticalRegularItem: NSCollectionLayoutItem { 468 | NSCollectionLayoutItem( 469 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 470 | heightDimension: .fractionalHeight(0.5))) 471 | } 472 | 473 | static var horizontalRegularItem: NSCollectionLayoutItem { 474 | NSCollectionLayoutItem( 475 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), 476 | heightDimension: .fractionalHeight(1.0))) 477 | } 478 | 479 | static var verticalRectangularItem: NSCollectionLayoutItem { 480 | NSCollectionLayoutItem( 481 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), 482 | heightDimension: .fractionalHeight(1.0))) 483 | } 484 | } 485 | 486 | // MARK:- Simple groups 487 | @available(iOS 13.0, *) 488 | public extension NSCollectionLayoutGroup { 489 | 490 | static func vertical2RegularItems(_ item: NSCollectionLayoutItem) -> NSCollectionLayoutGroup { 491 | NSCollectionLayoutGroup.vertical( 492 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), 493 | heightDimension: .fractionalHeight(1.0)), 494 | subitem: item, count: 2) 495 | } 496 | 497 | static func nestedHorizontalGroup(_ items: [NSCollectionLayoutItem]) -> NSCollectionLayoutGroup { 498 | NSCollectionLayoutGroup.horizontal( 499 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 500 | heightDimension: .fractionalWidth(2/3)), 501 | subitems: items) 502 | } 503 | 504 | static func horizontal3RegularItems(_ item: NSCollectionLayoutItem) -> NSCollectionLayoutGroup { 505 | NSCollectionLayoutGroup.horizontal( 506 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 507 | heightDimension: .fractionalHeight(1/3)), 508 | subitem: item, count: 3) 509 | } 510 | } 511 | 512 | @available(iOS 13.0, *) 513 | public extension NSDirectionalEdgeInsets { 514 | 515 | static func all(_ value: CGFloat) -> NSDirectionalEdgeInsets { 516 | .init(top: value, leading: value, bottom: value, trailing: value) 517 | } 518 | static var zero: Self { .all(0) } 519 | } 520 | 521 | public extension UIEdgeInsets { 522 | 523 | static func all(_ value: CGFloat) -> UIEdgeInsets { 524 | .init(top: value, left: value, bottom: value, right: value) 525 | } 526 | 527 | static var zero: Self { .all(0) } 528 | } 529 | 530 | 531 | 532 | 533 | 534 | 535 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Models/Feed/Feed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Feed.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct Author: Decodable { 11 | let name: String 12 | var uri: String? 13 | } 14 | 15 | public protocol ItunesResource: Decodable { 16 | associatedtype Model 17 | var title: String { get } 18 | var id: String { get } 19 | var author: Author { get } 20 | var copyright: String { get } 21 | var country: String { get } 22 | var icon: String { get } 23 | var updated: String { get } 24 | var results: [Model] { get } 25 | } 26 | 27 | public struct ItunesResources: ItunesResource { 28 | 29 | public let title: String 30 | public let id: String 31 | public let author: Author 32 | public let copyright: String 33 | public let country: String 34 | public let icon: String 35 | public let updated: String 36 | public let results: [Model] 37 | } 38 | 39 | public protocol FeedProtocol: Decodable { 40 | associatedtype FeedResource: ItunesResource 41 | var feed: FeedResource { get } 42 | } 43 | 44 | public struct Feed: FeedProtocol { 45 | public let feed: FeedResource 46 | } 47 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Models/Feed/FeedItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedItem.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Foundation 9 | import MarvelClient 10 | import Combine 11 | 12 | struct FeedItem: Decodable { 13 | 14 | public let artistName: String? 15 | public let id: String 16 | public let releaseDate: String? 17 | public let name: String 18 | public let kind: String 19 | public var copyright: String? 20 | public let artistId: String? 21 | public let artistUrl: String? 22 | public let artworkUrl100: String 23 | public let genres: [Genre] 24 | public let url: String 25 | } 26 | 27 | struct Genre: Decodable { 28 | let genreId: String? 29 | let name: String? 30 | let url: String? 31 | } 32 | 33 | public final class GenreViewModel: ObservableObject { 34 | 35 | @Published var genreId: String 36 | @Published var name: String 37 | @Published var url: String 38 | 39 | init(model: Genre) { 40 | genreId = model.genreId ?? "" 41 | name = model.name ?? "" 42 | url = model.url ?? "" 43 | } 44 | } 45 | 46 | public final class FeedItemViewModel: IdentifiableHashable, ObservableObject { 47 | 48 | @Published public var artistName: String? 49 | @Published public var id: String 50 | @Published public var releaseDate: String? 51 | @Published public var name: String 52 | @Published public var kind: String 53 | @Published public var copyright: String? 54 | @Published public var artistId: String? 55 | @Published public var artistUrl: String? 56 | @Published public var artworkUrl100: String 57 | @Published public var genres: [GenreViewModel] 58 | @Published public var url: URL 59 | @Published public var artworkURL: URL 60 | 61 | init(model: FeedItem) { 62 | artistName = model.artistName 63 | id = model.id 64 | releaseDate = model.releaseDate 65 | name = model.name 66 | kind = model.kind 67 | copyright = model.copyright 68 | artistId = model.artistId 69 | artistUrl = model.artistUrl 70 | artworkUrl100 = model.artworkUrl100 71 | genres = model.genres.map { GenreViewModel(model: $0) } 72 | url = URL(string: model.url)! 73 | artworkURL = URL(string: model.artworkUrl100)! 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Networking/CombineAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineAPI.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | import MarvelClient 11 | 12 | protocol CombineAPI { 13 | var session: URLSession { get } 14 | func execute(_ request: URLRequest, decodingType: T.Type, queue: DispatchQueue, retries: Int) -> AnyPublisher where T: Decodable 15 | } 16 | 17 | // 2 18 | extension CombineAPI { 19 | 20 | func execute(_ request: URLRequest, 21 | decodingType: T.Type, 22 | queue: DispatchQueue = .main, 23 | retries: Int = 0) -> AnyPublisher where T: Decodable { 24 | /// 3 25 | return session.dataTaskPublisher(for: request) 26 | .tryMap { 27 | guard let response = $0.response as? HTTPURLResponse, response.statusCode == 200 else { 28 | throw APIError.responseUnsuccessful(description: "\(String(describing: $0.response.url?.absoluteString))") 29 | } 30 | return $0.data 31 | } 32 | .decode(type: T.self, decoder: JSONDecoder()) 33 | .receive(on: queue) 34 | .retry(retries) 35 | .eraseToAnyPublisher() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Networking/FeedGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FeedGenerator.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Format: String { 11 | case json = ".json" 12 | case xml = ".xml" 13 | case atom = ".atom" 14 | } 15 | 16 | enum MediaType { 17 | 18 | case apps(contentType: AppsContentType, chart: AppsChart, limit: Int, format: Format) 19 | case audioBooks(contentType: AudioBooksContentType, chart: AudiobooksChart, limit: Int, format: Format) 20 | case music(contentType: MusicContentType, chart: MusicChart, limit: Int, format: Format) 21 | case books(contentType: BooksContentType, chart: BooksChart, limit: Int, format: Format) 22 | case podcasts(contentType: PodcastsContentType, chart: PodcastsChart, limit: Int, format: Format) 23 | 24 | var path: String { 25 | switch self { 26 | case .apps(let contentType, let chart, let limit, let format): return "/\(Itunes.ItunesMediaType.apps)/\(chart.rawValue)/\(limit)/\(contentType.rawValue)\(format.rawValue)" 27 | case .audioBooks(let contentType, let chart, let limit, let format): return "/\(Itunes.ItunesMediaType.audioBooks.rawValue)/\(chart.rawValue)/\(limit)/\(contentType.rawValue)\(format.rawValue)" 28 | case .music(let contentType, let chart, let limit, let format): return "/\(Itunes.ItunesMediaType.music.rawValue)/\(chart.rawValue)/\(limit)/\(contentType.rawValue)\(format.rawValue)" 29 | case .books(let contentType, let chart, let limit, let format): return "/\(Itunes.ItunesMediaType.books.rawValue)/\(chart.rawValue)/\(limit)/\(contentType.rawValue)\(format.rawValue)" 30 | case .podcasts(let contentType, let chart, let limit, let format): return "/\(Itunes.ItunesMediaType.podcasts.rawValue)/\(chart.rawValue)/\(limit)/\(contentType.rawValue)\(format.rawValue)" 31 | 32 | } 33 | } 34 | } 35 | 36 | /// Apps 37 | enum AppsContentType: String { 38 | case apps 39 | } 40 | 41 | enum AppsChart: String { 42 | case topPaid = "top-paid" 43 | case topFree = "top-free" 44 | } 45 | 46 | /// AudioBooks 47 | enum AudioBooksContentType: String { 48 | case audiobooks = "audio-books" 49 | } 50 | 51 | enum AudiobooksChart: String { 52 | case top 53 | } 54 | 55 | // Music 56 | enum MusicContentType: String { 57 | case albums 58 | case musicVideos = "music-videos" 59 | case playlists 60 | case songs 61 | } 62 | 63 | enum MusicChart: String { 64 | case mostPlayed = "most-played" 65 | } 66 | 67 | // Books 68 | enum BooksContentType: String { 69 | case books 70 | } 71 | 72 | enum BooksChart: String { 73 | case topPaid = "top-paid" 74 | case topFree = "top-free" 75 | } 76 | 77 | // Podcats 78 | enum PodcastsContentType: String { 79 | case episodes = "podcast-episodes" 80 | case podcasts 81 | } 82 | 83 | enum PodcastsChart: String { 84 | case top 85 | } 86 | 87 | struct Itunes { 88 | 89 | private var base: String { 90 | "https://rss.applemarketingtools.com" 91 | } 92 | 93 | var mediaTypePath: MediaType 94 | 95 | var urlComponents: URLComponents { 96 | var components = URLComponents(string: base)! //forceunwrapped becuase we know it exists 97 | components.path = "/api/v2/us" + mediaTypePath.path 98 | return components 99 | } 100 | 101 | var request: URLRequest { 102 | let url = urlComponents.url! //want to crash if no information is complete 103 | return URLRequest(url: url) 104 | } 105 | 106 | enum ItunesMediaType: String, CaseIterable { 107 | 108 | case apps 109 | case music 110 | case podcasts 111 | case books 112 | case audioBooks = "audio-books" 113 | 114 | var title: String { 115 | switch self { 116 | case .audioBooks: return "Audio Books" 117 | default: return rawValue.capitalized 118 | } 119 | } 120 | 121 | var imageSystemName: String { 122 | switch self { 123 | case .music: return "music.note.list" 124 | case .apps: return "apps.iphone" 125 | case .books: return "book" 126 | case .podcasts: return "dot.radiowaves.left.and.right" 127 | case .audioBooks: return "tv.music.note.fill" 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Networking/ItunesClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItunesClient.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import MarvelClient 11 | 12 | final class ItunesClient: CombineAPI { 13 | 14 | // 1 15 | let session: URLSession 16 | 17 | // 2 18 | init(configuration: URLSessionConfiguration) { 19 | self.session = URLSession(configuration: configuration) 20 | } 21 | 22 | convenience init() { 23 | self.init(configuration: .default) 24 | } 25 | 26 | // 3 27 | public func fetch(_ feed: Feed.Type, 28 | mediaType: MediaType) -> AnyPublisher { 29 | 30 | let itunes = Itunes(mediaTypePath: mediaType) 31 | print("PATH: \(String(describing: itunes.request.url?.absoluteString))") 32 | 33 | return execute(itunes.request, decodingType: feed) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Networking/ItunesRemote.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItunesRemote.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import CompositionalList 11 | import SwiftUI 12 | 13 | public struct GenericSectionIdentifierViewModel: SectionIdentifierViewModel { 14 | public var sectionIdentifier: SectionIdentifier 15 | public var cellIdentifiers: [CellIdentifier] 16 | } 17 | 18 | // Step 1: create a section identifier 19 | public enum SectionIdentifierExample: String, CaseIterable { 20 | case popular = "Popular" 21 | case new = "New" 22 | case top = "Top Items" 23 | case recent = "Recent" 24 | case comingSoon = "Coming Soon" 25 | } 26 | 27 | final class ItunesRemote: ObservableObject { 28 | 29 | private let service = ItunesClient() 30 | private var cancellable: AnyCancellable? 31 | 32 | @Published var feedItems: [GenericSectionIdentifierViewModel] = [] 33 | 34 | func fetchItems(_ mediaType: MediaType) { 35 | cancellable = service.fetch(Feed>.self, mediaType: mediaType).sink(receiveCompletion: { 36 | dump($0) 37 | }, receiveValue: { feed in 38 | let chunkCount = feed.feed.results.count / SectionIdentifierExample.allCases.count 39 | let chunks = feed.feed.results.map { FeedItemViewModel(model: $0) }.chunked(into: chunkCount) 40 | var sectionIdentifiers: [GenericSectionIdentifierViewModel] = [] 41 | for i in 0.. [[Element]] { 53 | return stride(from: 0, to: count, by: size).map { 54 | Array(self[$0 ..< Swift.min($0 + size, count)]) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Views/ArtWork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArtWork.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/12/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct ArtWork: View { 12 | 13 | @StateObject var artworkViewModel: FeedItemViewModel 14 | 15 | var body: some View { 16 | WebImage(url: artworkViewModel.artworkURL) 17 | // Supports options and context, like `.delayPlaceholder` to show placeholder only when error 18 | .onSuccess { image, data, cacheType in 19 | // Success 20 | // Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data 21 | } 22 | .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size 23 | .placeholder(Image(systemName: "photo")) // Placeholder Image 24 | // Supports ViewBuilder as well 25 | .placeholder { 26 | Rectangle().foregroundColor(.gray) 27 | } 28 | .indicator(.activity) // Activity Indicator 29 | .transition(.fade(duration: 0.5)) // Fade Transition with duration 30 | .frame(maxWidth: .infinity, maxHeight: .infinity) 31 | // .frame(idealWidth: 100, idealHeight: 100) 32 | // .scaledToFill() 33 | } 34 | } 35 | 36 | struct TileInfo: View { 37 | 38 | @ObservedObject var artworkViewModel: FeedItemViewModel 39 | 40 | var body: some View { 41 | 42 | VStack(alignment: .leading) { 43 | ArtWork(artworkViewModel: artworkViewModel) 44 | // .scaledToFill() 45 | //Rectangle() 46 | // .aspectRatio(1, contentMode: .fill) 47 | // .aspectRatio(CGSize(width: 100, height: 100), contentMode: .fill) 48 | .cornerRadius(5.0) 49 | VStack(alignment: .leading, spacing: 3) { 50 | Text(artworkViewModel.artistName ?? artworkViewModel.name) 51 | .modifier(PrimaryFootNote()) 52 | Text(artworkViewModel.genres.first?.name ?? "blob") 53 | .modifier(SecondaryFootNote()) 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct ListItem: View { 60 | 61 | @ObservedObject var artworkViewModel: FeedItemViewModel 62 | 63 | var body: some View { 64 | HStack(spacing: 8) { 65 | ArtWork(artworkViewModel: artworkViewModel) 66 | .frame(width: 50, height: 50) 67 | .clipped() 68 | .cornerRadius(3.0) 69 | VStack(alignment: .leading, spacing: 3) { 70 | Text(artworkViewModel.artistName ?? artworkViewModel.name) 71 | .modifier(PrimaryFootNote()) 72 | Text(artworkViewModel.genres.first?.name ?? "") 73 | .modifier(SecondaryFootNote()) 74 | } 75 | Spacer() 76 | } 77 | .frame(maxWidth: .infinity, maxHeight: .infinity) 78 | } 79 | } 80 | 81 | struct PrimaryTitle: ViewModifier { 82 | 83 | func body(content: Content) -> some View { 84 | content 85 | .foregroundColor(.primary) 86 | .font(.title) 87 | .lineLimit(1) 88 | .truncationMode(.tail) 89 | } 90 | } 91 | 92 | struct PrimaryFootNote: ViewModifier { 93 | 94 | func body(content: Content) -> some View { 95 | content 96 | .foregroundColor(.primary) 97 | .font(.callout) 98 | .lineLimit(1) 99 | .truncationMode(.tail) 100 | } 101 | } 102 | 103 | struct SecondaryFootNote: ViewModifier { 104 | 105 | func body(content: Content) -> some View { 106 | content 107 | .font(.footnote) 108 | .foregroundColor(.secondary) 109 | .lineLimit(1) 110 | .truncationMode(.tail) 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Itunes/Views/ItunesFeedItemDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItunesFeedItemDetailView.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/20/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ItunesFeedItemDetailView: View { 11 | 12 | @StateObject var viewModel: FeedItemViewModel 13 | 14 | var body: some View { 15 | ArtWork(artworkViewModel: viewModel) 16 | } 17 | } 18 | 19 | struct ItunesFeedItemDetailView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | ItunesFeedItemDetailView(viewModel: FeedItemViewModel(model: FeedItem(artistName: nil, id: "", releaseDate: nil, name: "", kind: "", copyright: "", artistId: "", artistUrl: "", artworkUrl100: "", genres: [], url: ""))) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Marvel/CarachterArtworkView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarachterArtworkView.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | import MarvelClient 11 | 12 | struct CarachterArtworkView: View { 13 | 14 | @ObservedObject var artworkViewModel: ArtworkViewModel 15 | var variant: ImageVariant 16 | 17 | var body: some View { 18 | let url = artworkViewModel.imagePathFor(variant: variant) 19 | WebImage(url: URL(string: url)) 20 | // Supports options and context, like `.delayPlaceholder` to show placeholder only when error 21 | .onSuccess { image, data, cacheType in 22 | // Success 23 | // Note: Data exist only when queried from disk cache or network. Use `.queryMemoryData` if you really need data 24 | } 25 | .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size 26 | .placeholder(Image(systemName: "photo")) // Placeholder Image 27 | // Supports ViewBuilder as well 28 | .placeholder { 29 | Rectangle().foregroundColor(.gray) 30 | } 31 | .indicator(.activity) // Activity Indicator 32 | .transition(.fade(duration: 0.5)) // Fade Transition with duration 33 | // .scaledToFit() 34 | .frame(maxWidth: .infinity, maxHeight: .infinity) 35 | // .aspectRatio(0.9, contentMode: .fit) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Helpers/Marvel/MarvelProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarvelProvider.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import Combine 9 | import MarvelClient 10 | 11 | final class MarvelProvider: ObservableObject { 12 | 13 | private let service = MarvelService(privateKey: "6905a8e2fb2033fdb10eea66645116669f1c4f04", publicKey: "27d25dbafd3ff80a9d448a19c11ace4d") 14 | 15 | @Published var series: [SerieViewModel] = [] 16 | @Published var characters: [CharacterViewModel] = [] 17 | @Published var comics: [ComicViewModel] = [] 18 | 19 | func fetchSeries() { 20 | service.fetch(MarvelData>.self) { resource in 21 | switch resource { 22 | case .success(let results): 23 | self.series = results.map { SerieViewModel(model: $0) } 24 | case .failure: break 25 | } 26 | } 27 | } 28 | 29 | func fetchCharacters() { 30 | service.fetch(MarvelData>.self) { resource in 31 | switch resource { 32 | case .success(let results): 33 | self.characters = results.map { CharacterViewModel(model: $0) } 34 | case .failure: break 35 | } 36 | } 37 | } 38 | 39 | func fetchComics() { 40 | service.fetch(MarvelData>.self) { resource in 41 | switch resource { 42 | case .success(let results): 43 | self.comics = results.map { ComicViewModel(model: $0) } 44 | case .failure: break 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UIApplicationSceneManifest 29 | 30 | UIApplicationSupportsMultipleScenes 31 | 32 | 33 | UIApplicationSupportsIndirectInputEvents 34 | 35 | UILaunchScreen 36 | 37 | UIRequiredDeviceCapabilities 38 | 39 | armv7 40 | 41 | UISupportedInterfaceOrientations 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | UISupportedInterfaceOrientations~ipad 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationPortraitUpsideDown 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExample/TabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBar.swift 3 | // CompositionalListExample 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import SwiftUI 9 | import CompositionalList 10 | 11 | 12 | struct TabBar: View { 13 | 14 | var body: some View { 15 | TabView { 16 | ForEach(0.. 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 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExampleUITests/CompositionalListExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalListExampleUITests.swift 3 | // CompositionalListExampleUITests 4 | // 5 | // Created by James Rochabrun on 1/11/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class CompositionalListExampleUITests: 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 | -------------------------------------------------------------------------------- /Example/CompositionalListExample/CompositionalListExampleUITests/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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James Rochabrun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CompositionalList", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "CompositionalList", 15 | targets: ["CompositionalList"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "CompositionalList", 26 | dependencies: []), 27 | .testTarget( 28 | name: "CompositionalListTests", 29 | dependencies: ["CompositionalList"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CompositionalList 🧩 2 | ![gradienta-ix_kUDzCczo-unsplash (1)](https://user-images.githubusercontent.com/5378604/105889098-a7e48e80-5fc2-11eb-87a3-91a4d21a003b.jpg) 3 | [![ForTheBadge built-with-love](http://ForTheBadge.com/images/badges/built-with-love.svg)](https://GitHub.com/Naereen/) 4 | [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Naereen/badges/) 5 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) 6 | [![swift-version](https://img.shields.io/badge/swift-5.1-brightgreen.svg)](https://github.com/apple/swift) 7 | [![swiftui-version](https://img.shields.io/badge/swiftui-brightgreen)](https://developer.apple.com/documentation/swiftui) 8 | [![xcode-version](https://img.shields.io/badge/xcode-11%20-brightgreen)](https://developer.apple.com/xcode/) 9 | [![swift-package-manager](https://img.shields.io/badge/package%20manager-compatible-brightgreen.svg?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNjJweCIgaGVpZ2h0PSI0OXB4IiB2aWV3Qm94PSIwIDAgNjIgNDkiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDYzLjEgKDkyNDUyKSAtIGh0dHBzOi8vc2tldGNoLmNvbSAtLT4KICAgIDx0aXRsZT5Hcm91cDwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGlkPSJHcm91cCIgZmlsbC1ydWxlPSJub256ZXJvIj4KICAgICAgICAgICAgPHBvbHlnb24gaWQ9IlBhdGgiIGZpbGw9IiNEQkI1NTEiIHBvaW50cz0iNTEuMzEwMzQ0OCAwIDEwLjY4OTY1NTIgMCAwIDEzLjUxNzI0MTQgMCA0OSA2MiA0OSA2MiAxMy41MTcyNDE0Ij48L3BvbHlnb24+CiAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJQYXRoIiBmaWxsPSIjRjdFM0FGIiBwb2ludHM9IjI3IDI1IDMxIDI1IDM1IDI1IDM3IDI1IDM3IDE0IDI1IDE0IDI1IDI1Ij48L3BvbHlnb24+CiAgICAgICAgICAgIDxwb2x5Z29uIGlkPSJQYXRoIiBmaWxsPSIjRUZDNzVFIiBwb2ludHM9IjEwLjY4OTY1NTIgMCAwIDE0IDYyIDE0IDUxLjMxMDM0NDggMCI+PC9wb2x5Z29uPgogICAgICAgICAgICA8cG9seWdvbiBpZD0iUmVjdGFuZ2xlIiBmaWxsPSIjRjdFM0FGIiBwb2ludHM9IjI3IDAgMzUgMCAzNyAxNCAyNSAxNCI+PC9wb2x5Z29uPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+)](https://github.com/apple/swift-package-manager) 10 | 11 | 12 | CompositionalList is a SwiftUI UIViewControllerRepresentable wrapper powered by UIKit DiffableDataSource and Compositional Layout. 🥸 13 | It is customizable and flexible and supports multiple sections and cell selection. It allows to use of any kind of SwiftUI view inside of cells, headers, or footers. 14 | 15 | # Requirements 16 | 17 | * iOS 13.0 or later 18 | 19 | # Features 20 | 21 | - [X] Supports multiple sections. 22 | - [X] Supports adapting UI to any kind of custom layout. 23 | - [X] Supports cell selection. 24 | 25 | CompositionalList adds `SwiftUI` views as children of `UICollectionViewCell's` and `UICollectionReusableView's` using `UIHostingController's`, it takes an array of data structures defined by a public protocol called `SectionIdentifierViewModel` that holds a section identifier and an array of cell identifiers. 26 | 27 | ```swift 28 | public protocol SectionIdentifierViewModel { 29 | associatedtype SectionIdentifier: Hashable 30 | associatedtype CellIdentifier: Hashable 31 | var sectionIdentifier: SectionIdentifier { get } 32 | var cellIdentifiers: [CellIdentifier] { get } 33 | } 34 | ``` 35 | 36 | `CompositionalList` basic structure looks like this... 37 | 38 | ```swift 39 | struct CompositionalList where ViewModel : SectionIdentifierViewModel, RowView : View, HeaderFooterView : View 40 | ``` 41 | 42 | * `ViewModel` must conform to `SectionIdentifierViewModel`. To satisfy this protocol you must create a data structure that contains a section identifier, for example, an enum, and an array of objects that conform to `Hashable`. 43 | * `RowView` the compiler will infer the return value in the `CellProvider` closure as long it conforms to `View`. 44 | * `HeaderFooterView` must conform to `View`, which represents a header or a footer in a section. The developer must provide a view to satisfying the generic parameter. By now we need to return any kind of `View` to avoid the compiler force us to define the Types on initialization, if a header is not needed return a `Spacer` with a height of `0`. 45 | 46 | # Getting Started 47 | 48 | * Read this Readme doc 49 | * Read the How to use section. 50 | * Clone the [Example](https://github.com/jamesrochabrun/CompositionalList/tree/main/Example/CompositionalListExample) project as needed. 51 | 52 | # How to use. 53 | 54 | `CompositionalList` is initialized with an array of data structures that conform to `SectionIdentifierViewModel` which represents a section, this means it can have one or X number of sections. 55 | 56 | - **Step 1**, create a section identifier like this... 57 | 58 | ```swift 59 | public enum SectionIdentifierExample: String, CaseIterable { 60 | case popular = "Popular" 61 | case new = "New" 62 | case top = "Top Items" 63 | case recent = "Recent" 64 | case comingSoon = "Coming Soon" 65 | } 66 | ``` 67 | 68 | - **Step 2**, create a data structure that conforms to `SectionIdentifierViewModel`... 69 | 70 | ```swift 71 | struct FeedSectionIdentifier: SectionIdentifierViewModel { 72 | let sectionIdentifier: SectionIdentifierExample // <- This is your identifier for each section. 73 | let cellIdentifiers: [FeedItemViewModel] // <- This is your model for each cell. 74 | } 75 | ``` 76 | 77 | - **Step 3**, creating a section, can be done inside a data provider view model that conforms to `ObservableObject`. 😉 78 | 79 | _For simplicity, here we are creating a single section, for the full code on how to create multiple sections check the [example source code](https://github.com/jamesrochabrun/CompositionalList/tree/main/Example/CompositionalListExample)._ 80 | 81 | ```swift 82 | struct Remote: ObservableObject { 83 | 84 | @Published var sectionIdentifiers: [FeedSectionIdentifier] 85 | 86 | func fetch() { 87 | /// your code for fetching some models... 88 | sectionIdentifiers = [FeedSectionIdentifier(sectionIdentifier: .popular, cellIdentifiers: models)] 89 | } 90 | } 91 | ``` 92 | 93 | - **Step4** 🤖, initialize the `CompositionalList` with the array of section identifiers... 94 | 95 | 96 | ```swift 97 | import CompositionalList 98 | 99 | ..... 100 | 101 | @ObservedObject private var remote = Remote() 102 | 103 | var body: some View { 104 | NavigationView { 105 | /// 5 106 | if items.isEmpty { 107 | ActivityIndicator() 108 | } else { 109 | CompositionalList(remote.sectionIdentifiers) { model, indexPath in 110 | /// 1 111 | Group { 112 | switch indexPath.section { 113 | case 0, 2, 3: 114 | TileInfo(artworkViewModel: model) 115 | case 1: 116 | ListItem(artworkViewModel: model) 117 | default: 118 | ArtWork(artworkViewModel: model) 119 | } 120 | } 121 | }.sectionHeader { sectionIdentifier, kind, indexPath in 122 | /// 2 123 | TitleHeaderView(title: sectionIdentifier?.rawValue ?? "") 124 | } 125 | .selectedItem { 126 | /// 3 127 | selectedItem = $0 128 | } 129 | /// 4 130 | .customLayout(.composed()) 131 | } 132 | }.onAppear { 133 | remote.fetch() 134 | } 135 | } 136 | ``` 137 | 138 | 1. `CellProvider` closure that provides a `model` and an `indexpath` and expects a `View` as the return value. Here you can return different `SwiftUI` views for each section, if you use a conditional statement like a `Switch` in this case, you must use a `Group` as the return value. For example in this case the compiler will infer this as the return value: 139 | 140 | ```swift 141 | Group<_ConditionalContent<_ConditionalContent, ArtWork>> 142 | ``` 143 | 144 | 2. `HeaderFooterProvider` closure that provides the section identifier, the `kind` which can be `UICollectionView.elementKindSectionHeader` or `UICollectionView.elementKindSectionFooter` this will be defined by your layout, and the indexPath for the corresponding section. It expects a `View` as a return value, you can customize your return value based on the section or if it's a header or a footer. Same as `CellProvider` if a conditional statement is used make sure to wrap it in a `Group`. This closure is required even If you don't define headers or footers in your layout you still need to return a `View`, in that case, you can return a `Spacer` with a height of 0. (looking for a more elegant solution by now 🤷🏽‍♂️). 145 | 146 | 3. `SelectionProvider` closure, internally uses `UICollectionViewDelegate` cell did select a method to provide the selected item, this closure is optional. 147 | 148 | 4. `customLayout` environment object, here you can return any kind of layout as long is a `UICollectionViewLayout`. You can find the code for the layout [here](https://github.com/jamesrochabrun/CompositionalList/blob/main/Sources/CompositionalList/UIKit/Layout%2BUtils.swift). 😉 149 | 150 | 5. For a reason that I still don't understand, we need to use a conditional statement verifying that the array is not empty, is handy for this case because we can return a spinner. 😬 151 | 152 | # Installation 153 | 154 | Installation with Swift Package Manager (Xcode 11+) 155 | Swift Package Manager (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode. 156 | 157 | CompositionalList supports SwiftPM from version 5.1.0. To use SwiftPM, you should use Xcode 11 to open your project. `Click File` -> `Swift Packages` -> `Add Package Dependency,` enter CompositionalList repo's [URL](https://github.com/jamesrochabrun/CompositionalList). Or you can log in to Xcode with your GitHub account and just type CompositionalList to search. 158 | 159 | After selecting the package, you can choose the dependency type (tagged version, branch, or commit). Then Xcode will set up all the stuff for you. 160 | 161 | # How To Collaborate 162 | 163 | * This repo contains a convenient Compositional Layout extension to compose different layouts, feel free to add more layouts! 164 | * Open a PR for any proposed change pointing it to `main` branch. 165 | 166 | ### DEMO 167 | 168 | ![k1](https://user-images.githubusercontent.com/5378604/105805472-ecd2db80-5f56-11eb-9a11-787cc9f746bc.gif) 169 | 170 | # **Important**: 171 | 172 | Folow the [Example](https://github.com/jamesrochabrun/CompositionalList/tree/main/Example/CompositionalListExample) project 🤓 173 | 174 | CompositionalList is open source, feel free to collaborate! 175 | 176 | TODO: 177 | 178 | - [ ] Improve loading data, `UIVIewRepresentable` does not update its context, need to investigate why. 179 | - [ ] Investigate why we need to make a conditional statement checking if the data is empty inside the view. 180 | -------------------------------------------------------------------------------- /Sources/CompositionalList/SwiftUI/CompositionalList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionalList.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | /// `UIViewRepresentable` object that takes a `View` and a `Model` to render items in a list, it takes a `UICollectionViewLayout` from an environment object. 13 | 14 | /** 15 | - `ViewModel` must conform to `SectionIdentifierViewModel` ` 16 | - `RowView` must conform to `View`, represents a cell. 17 | - `HeaderFooterView` must conform to `View`, represents a header or a footer. Dev must provide a view to satisfy the generic parameter, if a header 18 | is not needed return a `Spacer` with height of `0` 19 | - `SelectionProvider` provides the view model associated with the selected cell. This is optional 20 | - 21 | */ 22 | @available(iOS 13, *) 23 | public struct CompositionalList { 26 | 27 | public typealias Diff = DiffCollectionView 28 | public typealias SelectionProvider = ((ViewModel.CellIdentifier) -> Void) 29 | 30 | @Environment(\.layout) var customLayout 31 | 32 | var itemsPerSection: [ViewModel] 33 | let cellProvider: Diff.CellProvider 34 | var selectionProvider: SelectionProvider? 35 | 36 | private (set)var headerProvider: Diff.HeaderFooterProvider? = nil 37 | 38 | public init(_ items: [ViewModel], 39 | @ViewBuilder cellProvider: @escaping Diff.CellProvider) { 40 | self.cellProvider = cellProvider 41 | self.itemsPerSection = items 42 | } 43 | 44 | public func makeCoordinator() -> Coordinator { 45 | Coordinator(self) 46 | } 47 | 48 | public final class Coordinator: NSObject, UICollectionViewDelegate { 49 | 50 | fileprivate let list: CompositionalList 51 | fileprivate var itemsPerSection: [ViewModel] 52 | fileprivate let cellProvider: Diff.CellProvider 53 | 54 | fileprivate let layout: UICollectionViewLayout 55 | fileprivate let headerProvider: Diff.HeaderFooterProvider? 56 | fileprivate let selectionProvider: SelectionProvider? 57 | 58 | init(_ list: CompositionalList) { 59 | 60 | self.list = list 61 | self.layout = list.customLayout 62 | self.cellProvider = list.cellProvider 63 | self.headerProvider = list.headerProvider 64 | self.itemsPerSection = list.itemsPerSection 65 | self.selectionProvider = list.selectionProvider 66 | } 67 | 68 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 69 | let sectionIdentifier = itemsPerSection[indexPath.section] 70 | let model = sectionIdentifier.cellIdentifiers[indexPath.item] 71 | selectionProvider?(model) 72 | } 73 | } 74 | } 75 | 76 | @available(iOS 13, *) 77 | extension CompositionalList: UIViewControllerRepresentable { 78 | 79 | public func makeUIViewController(context: Context) -> Diff { 80 | Diff(layout: context.coordinator.layout, 81 | collectionViewDelegate: context.coordinator, 82 | context.coordinator.cellProvider, 83 | context.coordinator.headerProvider) 84 | } 85 | 86 | public func updateUIViewController(_ uiViewController: Diff, context: Context) { 87 | uiViewController.applySnapshotWith(context.coordinator.itemsPerSection) 88 | } 89 | } 90 | 91 | @available(iOS 13, *) 92 | extension CompositionalList { 93 | 94 | public func sectionHeader(_ header: @escaping Diff.HeaderFooterProvider) -> Self { 95 | var `self` = self 96 | `self`.headerProvider = header 97 | return `self` 98 | } 99 | 100 | public func selectedItem(_ selectionProvider: SelectionProvider?) -> Self { 101 | var `self` = self 102 | `self`.selectionProvider = selectionProvider 103 | return `self` 104 | } 105 | } 106 | 107 | ///// Environment 108 | @available(iOS 13, *) 109 | public struct Layout: EnvironmentKey { 110 | public static var defaultValue: UICollectionViewLayout = UICollectionViewLayout() 111 | } 112 | 113 | @available(iOS 13, *) 114 | extension EnvironmentValues { 115 | var layout: UICollectionViewLayout { 116 | get { self[Layout.self] } 117 | set { self[Layout.self] = newValue } 118 | } 119 | } 120 | 121 | @available(iOS 13, *) 122 | public extension CompositionalList { 123 | func customLayout(_ layout: UICollectionViewLayout) -> some View { 124 | environment(\.layout, layout) 125 | } 126 | } 127 | 128 | 129 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/BaseCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseCollectionViewCell.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | 10 | open class BaseCollectionViewCell: UICollectionViewCell { 11 | 12 | public var viewModel: V? { 13 | didSet { 14 | guard let viewModel = viewModel else { return } 15 | setupWith(viewModel) 16 | } 17 | } 18 | 19 | public override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | setupSubviews() 22 | } 23 | 24 | required public init?(coder aDecoder: NSCoder) { 25 | super.init(coder: aDecoder) 26 | } 27 | 28 | public override func awakeFromNib() { 29 | super.awakeFromNib() 30 | self.setupSubviews() 31 | } 32 | 33 | // To be overriden. Super does not need to be called. 34 | open func setupSubviews() { 35 | } 36 | 37 | // To be overriden. Super does not need to be called. 38 | open func setupWith(_ viewModel: V) { 39 | } 40 | 41 | /// Swift UI 42 | open func setupWith(_ viewModel: V, parent: UIViewController?) { 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/CollectionReusable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionReusable.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | 10 | public protocol CollectionReusable {} 11 | 12 | public extension CollectionReusable where Self: UITableViewCell { 13 | static var reuseIdentifier: String { 14 | return String(describing: self) 15 | } 16 | } 17 | 18 | /// MARK:- UICollectionView 19 | public extension CollectionReusable where Self: UICollectionViewCell { 20 | static var reuseIdentifier: String { 21 | return String(describing: self) 22 | } 23 | } 24 | 25 | public extension UICollectionView { 26 | 27 | /// Register Programatic Cell 28 | func register(_ :T.Type) { 29 | register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier) 30 | } 31 | 32 | /// Register Xib cell 33 | func registerNib(_ :T.Type, in bundle: Bundle? = nil) { 34 | let nib = UINib(nibName: T.reuseIdentifier, bundle: bundle) 35 | register(nib, forCellWithReuseIdentifier: T.reuseIdentifier) 36 | } 37 | 38 | func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T { 39 | let cell = dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T 40 | return cell 41 | } 42 | 43 | /// Register Programatic Header 44 | func registerHeader(_ :T.Type, kind: String) { 45 | register(T.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier) 46 | } 47 | 48 | /// Register Xib Header 49 | func registerNibHeader(_ : T.Type, kind: String, in bundle: Bundle? = nil) { 50 | let nib = UINib(nibName: T.reuseIdentifier, bundle: bundle) 51 | register(nib, forSupplementaryViewOfKind: kind, withReuseIdentifier: T.reuseIdentifier) 52 | 53 | } 54 | 55 | func dequeueSuplementaryView(of kind: String, at indexPath: IndexPath) -> T { 56 | let supplementaryView = dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T 57 | return supplementaryView 58 | } 59 | } 60 | 61 | /// MARK:- UICollectionView 62 | public extension CollectionReusable where Self: UICollectionReusableView { 63 | static var reuseIdentifier: String { 64 | return String(describing: self) 65 | } 66 | } 67 | 68 | extension UICollectionReusableView: CollectionReusable {} 69 | 70 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/DiffCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiffCollectionView.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | /** 12 | Protocol that represents a section in a collectionView data source. 13 | */ 14 | public protocol SectionIdentifierViewModel { 15 | associatedtype SectionIdentifier: Hashable 16 | associatedtype CellIdentifier: Hashable 17 | var sectionIdentifier: SectionIdentifier { get } 18 | var cellIdentifiers: [CellIdentifier] { get } 19 | } 20 | 21 | /// Helper 22 | public struct GenericSectionIdentifierViewModel: SectionIdentifierViewModel { 23 | public var sectionIdentifier: SectionIdentifier? = nil 24 | public var cellIdentifiers: [CellIdentifier] 25 | } 26 | 27 | @available(iOS 13, *) 28 | public final class DiffCollectionView: UIViewController { 31 | 32 | // MARK:- Private 33 | private (set)var collectionView: UICollectionView! // if not initilaized, lets crash. 🤷🏽‍♂️ 34 | private typealias DiffDataSource = UICollectionViewDiffableDataSource 35 | private var dataSource: DiffDataSource? 36 | private typealias Snapshot = NSDiffableDataSourceSnapshot 37 | private var currentSnapshot: Snapshot? 38 | 39 | // MARK:- Public 40 | public typealias CellProvider = (ViewModel.CellIdentifier, IndexPath) -> RowView 41 | public typealias HeaderFooterProvider = (ViewModel.SectionIdentifier, String, IndexPath) -> HeaderFooterView? 42 | public typealias SelectedContentAtIndexPath = ((ViewModel.CellIdentifier, IndexPath) -> Void) 43 | public var selectedContentAtIndexPath: SelectedContentAtIndexPath? 44 | 45 | // MARK:- Life Cycle 46 | convenience init(layout: UICollectionViewLayout, 47 | collectionViewDelegate: UICollectionViewDelegate, 48 | @ViewBuilder _ cellProvider: @escaping CellProvider, 49 | _ headerFooterProvider: HeaderFooterProvider?) { 50 | self.init() 51 | collectionView = .init(frame: .zero, collectionViewLayout: layout) 52 | collectionView.backgroundColor = .clear 53 | collectionView.register(WrapperViewCell.self) 54 | collectionView.registerHeader(WrapperCollectionReusableView.self, kind: UICollectionView.elementKindSectionHeader) 55 | collectionView.registerHeader(WrapperCollectionReusableView.self, kind: UICollectionView.elementKindSectionFooter) 56 | collectionView.delegate = collectionViewDelegate 57 | view.addSubview(collectionView) 58 | collectionView.translatesAutoresizingMaskIntoConstraints = false 59 | NSLayoutConstraint.activate([ 60 | collectionView.topAnchor.constraint(equalTo: view.topAnchor), 61 | collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 62 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 63 | collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 64 | ]) 65 | configureDataSource(cellProvider) 66 | if let headerFooterProvider = headerFooterProvider { 67 | assignHedearFooter(headerFooterProvider) 68 | } 69 | } 70 | 71 | // MARK:- DataSource Configuration 72 | private func configureDataSource(_ cellProvider: @escaping CellProvider) { 73 | 74 | dataSource = DiffDataSource(collectionView: collectionView) { [weak self] collectionView, indexPath, model in 75 | let cell: WrapperViewCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) 76 | let cellView = cellProvider(model, indexPath) 77 | cell.setupWith(cellView, parent: self) 78 | return cell 79 | } 80 | } 81 | 82 | // MARK:- ViewModel injection and snapshot 83 | public func applySnapshotWith(_ itemsPerSection: [ViewModel]) { 84 | currentSnapshot = Snapshot() 85 | guard var currentSnapshot = currentSnapshot else { return } 86 | currentSnapshot.appendSections(itemsPerSection.map { $0.sectionIdentifier }) 87 | itemsPerSection.forEach { currentSnapshot.appendItems($0.cellIdentifiers, toSection: $0.sectionIdentifier) } 88 | dataSource?.apply(currentSnapshot) 89 | } 90 | 91 | private func assignHedearFooter(_ headerFooterProvider: @escaping HeaderFooterProvider) { 92 | 93 | dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in 94 | let header: WrapperCollectionReusableView = collectionView.dequeueSuplementaryView(of: kind, at: indexPath) 95 | if let sectionIdentifier = self?.dataSource?.snapshot().sectionIdentifiers[indexPath.section], 96 | let view = headerFooterProvider(sectionIdentifier, kind, indexPath) { 97 | header.setupWith(view, parent: self) 98 | } 99 | return header 100 | } 101 | } 102 | } 103 | 104 | @available(iOS 13.0, *) 105 | // MARK:- Helper 106 | extension NSDiffableDataSourceSnapshot { 107 | 108 | mutating func deleteItems(_ items: [ItemIdentifierType], at section: Int) { 109 | 110 | deleteItems(items) 111 | let sectionIdentifier = sectionIdentifiers[section] 112 | guard numberOfItems(inSection: sectionIdentifier) == 0 else { return } 113 | deleteSections([sectionIdentifier]) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/HostView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HostView.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | /// UIView abstraction that hosts a SwfitUI `View` 12 | 13 | @available(iOS 13, *) 14 | final public class HostView: UIView { 15 | 16 | private weak var controller: UIHostingController? 17 | 18 | public init(parent: UIViewController?, view: V) { 19 | super.init(frame: .zero) 20 | host(view, in: parent) 21 | } 22 | 23 | required public init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | } 26 | 27 | private func host(_ view: V, in parent: UIViewController?) { 28 | 29 | defer { controller?.view.invalidateIntrinsicContentSize() } 30 | 31 | if let controller = controller { 32 | controller.rootView = view 33 | } else { 34 | let hostingController = UIHostingController(rootView: view) 35 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 36 | controller = hostingController 37 | parent?.addChild(hostingController) 38 | addSubview(hostingController.view) 39 | NSLayoutConstraint.activate ([ 40 | hostingController.view.topAnchor.constraint(equalTo: topAnchor), 41 | hostingController.view.leadingAnchor.constraint(equalTo: leadingAnchor), 42 | hostingController.view.bottomAnchor.constraint(equalTo: bottomAnchor), 43 | hostingController.view.trailingAnchor.constraint(equalTo: trailingAnchor) 44 | ]) 45 | hostingController.didMove(toParent: parent) 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/WrapperCollectionReusableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapperCollectionReusableView.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | /// UICollectionReusableView abstraction that hosts a SwfitUI `View` 12 | 13 | @available(iOS 13, *) 14 | final public class WrapperCollectionReusableView: UICollectionReusableView { 15 | 16 | private var hostView: HostView? 17 | 18 | public func setupWith(_ view: V, parent: UIViewController?) { 19 | hostView = HostView(parent: parent, view: view) 20 | guard let hostView = hostView else { return } 21 | hostView.translatesAutoresizingMaskIntoConstraints = false 22 | addSubview(hostView) 23 | NSLayoutConstraint.activate([ 24 | hostView.topAnchor.constraint(equalTo: topAnchor), 25 | hostView.leadingAnchor.constraint(equalTo: leadingAnchor), 26 | hostView.bottomAnchor.constraint(equalTo: bottomAnchor), 27 | hostView.trailingAnchor.constraint(equalTo: trailingAnchor) 28 | ]) 29 | } 30 | 31 | public override func prepareForReuse() { 32 | super.prepareForReuse() 33 | hostView?.removeFromSuperview() 34 | hostView = nil 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CompositionalList/UIKit/WrapperViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapperViewCell.swift 3 | // CompositionalList 4 | // 5 | // Created by James Rochabrun on 1/10/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | /// UICollectionviewCell abstraction that hosts a SwfitUI `View` 12 | @available(iOS 13.0, *) 13 | final public class WrapperViewCell: BaseCollectionViewCell { 14 | 15 | private var hostView: HostView? 16 | 17 | public override func setupWith(_ viewModel: V, parent: UIViewController?) { 18 | hostView = HostView(parent: parent, view: viewModel) 19 | guard let hostView = hostView else { return } 20 | contentView.addSubview(hostView) 21 | hostView.translatesAutoresizingMaskIntoConstraints = false 22 | NSLayoutConstraint.activate([ 23 | hostView.topAnchor.constraint(equalTo: contentView.topAnchor), 24 | hostView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 25 | hostView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 26 | hostView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) 27 | ]) 28 | } 29 | 30 | public override func prepareForReuse() { 31 | super.prepareForReuse() 32 | hostView?.removeFromSuperview() 33 | hostView = nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/CompositionalListTests/CompositionalListTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CompositionalList 3 | 4 | final class CompositionalListTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | XCTAssertEqual(CompositionalList().text, "Hello, World!") 10 | } 11 | 12 | static var allTests = [ 13 | ("testExample", testExample), 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CompositionalListTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(CompositionalListTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import CompositionalListTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += CompositionalListTests.allTests() 7 | XCTMain(tests) 8 | --------------------------------------------------------------------------------