├── .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 | 
3 | [](https://GitHub.com/Naereen/)
4 | [](https://github.com/Naereen/badges/)
5 | [](https://lbesson.mit-license.org/)
6 | [](https://github.com/apple/swift)
7 | [](https://developer.apple.com/documentation/swiftui)
8 | [](https://developer.apple.com/xcode/)
9 | [](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 | 
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 |
--------------------------------------------------------------------------------