├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── Example.xcodeproj
│ ├── Example.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── Example.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Example
│ ├── ExampleApp.swift
│ ├── Info.plist
│ └── Shelf.swift
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── SwiftUICollection
└── CollectionView.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/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 6F80BED92510DF8D00192322 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80BED82510DF8D00192322 /* ExampleApp.swift */; };
11 | 6F80BEE82510E1F700192322 /* Shelf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80BEE72510E1F700192322 /* Shelf.swift */; };
12 | 6F80BEEB2510E22F00192322 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = 6F80BEEA2510E22F00192322 /* SwiftUICollection */; };
13 | /* End PBXBuildFile section */
14 |
15 | /* Begin PBXFileReference section */
16 | 6F80BED52510DF8D00192322 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
17 | 6F80BED82510DF8D00192322 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; };
18 | 6F80BEE12510DF8F00192322 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
19 | 6F80BEE72510E1F700192322 /* Shelf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shelf.swift; sourceTree = ""; };
20 | /* End PBXFileReference section */
21 |
22 | /* Begin PBXFrameworksBuildPhase section */
23 | 6F80BED22510DF8D00192322 /* Frameworks */ = {
24 | isa = PBXFrameworksBuildPhase;
25 | buildActionMask = 2147483647;
26 | files = (
27 | 6F80BEEB2510E22F00192322 /* SwiftUICollection in Frameworks */,
28 | );
29 | runOnlyForDeploymentPostprocessing = 0;
30 | };
31 | /* End PBXFrameworksBuildPhase section */
32 |
33 | /* Begin PBXGroup section */
34 | 6F80BECC2510DF8D00192322 = {
35 | isa = PBXGroup;
36 | children = (
37 | 6F80BED72510DF8D00192322 /* Example */,
38 | 6F80BED62510DF8D00192322 /* Products */,
39 | 6F80BEE92510E22F00192322 /* Frameworks */,
40 | );
41 | sourceTree = "";
42 | };
43 | 6F80BED62510DF8D00192322 /* Products */ = {
44 | isa = PBXGroup;
45 | children = (
46 | 6F80BED52510DF8D00192322 /* Example.app */,
47 | );
48 | name = Products;
49 | sourceTree = "";
50 | };
51 | 6F80BED72510DF8D00192322 /* Example */ = {
52 | isa = PBXGroup;
53 | children = (
54 | 6F80BEE72510E1F700192322 /* Shelf.swift */,
55 | 6F80BED82510DF8D00192322 /* ExampleApp.swift */,
56 | 6F80BEE12510DF8F00192322 /* Info.plist */,
57 | );
58 | path = Example;
59 | sourceTree = "";
60 | };
61 | 6F80BEE92510E22F00192322 /* Frameworks */ = {
62 | isa = PBXGroup;
63 | children = (
64 | );
65 | name = Frameworks;
66 | sourceTree = "";
67 | };
68 | /* End PBXGroup section */
69 |
70 | /* Begin PBXNativeTarget section */
71 | 6F80BED42510DF8D00192322 /* Example */ = {
72 | isa = PBXNativeTarget;
73 | buildConfigurationList = 6F80BEE42510DF8F00192322 /* Build configuration list for PBXNativeTarget "Example" */;
74 | buildPhases = (
75 | 6F80BED12510DF8D00192322 /* Sources */,
76 | 6F80BED22510DF8D00192322 /* Frameworks */,
77 | 6F80BED32510DF8D00192322 /* Resources */,
78 | );
79 | buildRules = (
80 | );
81 | dependencies = (
82 | );
83 | name = Example;
84 | packageProductDependencies = (
85 | 6F80BEEA2510E22F00192322 /* SwiftUICollection */,
86 | );
87 | productName = Example;
88 | productReference = 6F80BED52510DF8D00192322 /* Example.app */;
89 | productType = "com.apple.product-type.application";
90 | };
91 | /* End PBXNativeTarget section */
92 |
93 | /* Begin PBXProject section */
94 | 6F80BECD2510DF8D00192322 /* Project object */ = {
95 | isa = PBXProject;
96 | attributes = {
97 | LastSwiftUpdateCheck = 1200;
98 | LastUpgradeCheck = 1200;
99 | TargetAttributes = {
100 | 6F80BED42510DF8D00192322 = {
101 | CreatedOnToolsVersion = 12.0;
102 | };
103 | };
104 | };
105 | buildConfigurationList = 6F80BED02510DF8D00192322 /* Build configuration list for PBXProject "Example" */;
106 | compatibilityVersion = "Xcode 9.3";
107 | developmentRegion = en;
108 | hasScannedForEncodings = 0;
109 | knownRegions = (
110 | en,
111 | Base,
112 | );
113 | mainGroup = 6F80BECC2510DF8D00192322;
114 | productRefGroup = 6F80BED62510DF8D00192322 /* Products */;
115 | projectDirPath = "";
116 | projectRoot = "";
117 | targets = (
118 | 6F80BED42510DF8D00192322 /* Example */,
119 | );
120 | };
121 | /* End PBXProject section */
122 |
123 | /* Begin PBXResourcesBuildPhase section */
124 | 6F80BED32510DF8D00192322 /* Resources */ = {
125 | isa = PBXResourcesBuildPhase;
126 | buildActionMask = 2147483647;
127 | files = (
128 | );
129 | runOnlyForDeploymentPostprocessing = 0;
130 | };
131 | /* End PBXResourcesBuildPhase section */
132 |
133 | /* Begin PBXSourcesBuildPhase section */
134 | 6F80BED12510DF8D00192322 /* Sources */ = {
135 | isa = PBXSourcesBuildPhase;
136 | buildActionMask = 2147483647;
137 | files = (
138 | 6F80BEE82510E1F700192322 /* Shelf.swift in Sources */,
139 | 6F80BED92510DF8D00192322 /* ExampleApp.swift in Sources */,
140 | );
141 | runOnlyForDeploymentPostprocessing = 0;
142 | };
143 | /* End PBXSourcesBuildPhase section */
144 |
145 | /* Begin XCBuildConfiguration section */
146 | 6F80BEE22510DF8F00192322 /* Debug */ = {
147 | isa = XCBuildConfiguration;
148 | buildSettings = {
149 | ALWAYS_SEARCH_USER_PATHS = NO;
150 | CLANG_ANALYZER_NONNULL = YES;
151 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
152 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
153 | CLANG_CXX_LIBRARY = "libc++";
154 | CLANG_ENABLE_MODULES = YES;
155 | CLANG_ENABLE_OBJC_ARC = YES;
156 | CLANG_ENABLE_OBJC_WEAK = YES;
157 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
158 | CLANG_WARN_BOOL_CONVERSION = YES;
159 | CLANG_WARN_COMMA = YES;
160 | CLANG_WARN_CONSTANT_CONVERSION = YES;
161 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
162 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
163 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
164 | CLANG_WARN_EMPTY_BODY = YES;
165 | CLANG_WARN_ENUM_CONVERSION = YES;
166 | CLANG_WARN_INFINITE_RECURSION = YES;
167 | CLANG_WARN_INT_CONVERSION = YES;
168 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
169 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
170 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
171 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
172 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
173 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
174 | CLANG_WARN_STRICT_PROTOTYPES = YES;
175 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
176 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
177 | CLANG_WARN_UNREACHABLE_CODE = YES;
178 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
179 | COPY_PHASE_STRIP = NO;
180 | DEBUG_INFORMATION_FORMAT = dwarf;
181 | ENABLE_STRICT_OBJC_MSGSEND = YES;
182 | ENABLE_TESTABILITY = YES;
183 | GCC_C_LANGUAGE_STANDARD = gnu11;
184 | GCC_DYNAMIC_NO_PIC = NO;
185 | GCC_NO_COMMON_BLOCKS = YES;
186 | GCC_OPTIMIZATION_LEVEL = 0;
187 | GCC_PREPROCESSOR_DEFINITIONS = (
188 | "DEBUG=1",
189 | "$(inherited)",
190 | );
191 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
192 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
193 | GCC_WARN_UNDECLARED_SELECTOR = YES;
194 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
195 | GCC_WARN_UNUSED_FUNCTION = YES;
196 | GCC_WARN_UNUSED_VARIABLE = YES;
197 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
198 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
199 | MTL_FAST_MATH = YES;
200 | ONLY_ACTIVE_ARCH = YES;
201 | SDKROOT = iphoneos;
202 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos";
203 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
204 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
205 | TARGETED_DEVICE_FAMILY = "1,2,3";
206 | TVOS_DEPLOYMENT_TARGET = 14.0;
207 | };
208 | name = Debug;
209 | };
210 | 6F80BEE32510DF8F00192322 /* Release */ = {
211 | isa = XCBuildConfiguration;
212 | buildSettings = {
213 | ALWAYS_SEARCH_USER_PATHS = NO;
214 | CLANG_ANALYZER_NONNULL = YES;
215 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
217 | CLANG_CXX_LIBRARY = "libc++";
218 | CLANG_ENABLE_MODULES = YES;
219 | CLANG_ENABLE_OBJC_ARC = YES;
220 | CLANG_ENABLE_OBJC_WEAK = YES;
221 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
222 | CLANG_WARN_BOOL_CONVERSION = YES;
223 | CLANG_WARN_COMMA = YES;
224 | CLANG_WARN_CONSTANT_CONVERSION = YES;
225 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
226 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
227 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
228 | CLANG_WARN_EMPTY_BODY = YES;
229 | CLANG_WARN_ENUM_CONVERSION = YES;
230 | CLANG_WARN_INFINITE_RECURSION = YES;
231 | CLANG_WARN_INT_CONVERSION = YES;
232 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
233 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
234 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
235 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
236 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
237 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
238 | CLANG_WARN_STRICT_PROTOTYPES = YES;
239 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
240 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
241 | CLANG_WARN_UNREACHABLE_CODE = YES;
242 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
243 | COPY_PHASE_STRIP = NO;
244 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
245 | ENABLE_NS_ASSERTIONS = NO;
246 | ENABLE_STRICT_OBJC_MSGSEND = YES;
247 | GCC_C_LANGUAGE_STANDARD = gnu11;
248 | GCC_NO_COMMON_BLOCKS = YES;
249 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
250 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
251 | GCC_WARN_UNDECLARED_SELECTOR = YES;
252 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
253 | GCC_WARN_UNUSED_FUNCTION = YES;
254 | GCC_WARN_UNUSED_VARIABLE = YES;
255 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
256 | MTL_ENABLE_DEBUG_INFO = NO;
257 | MTL_FAST_MATH = YES;
258 | SDKROOT = iphoneos;
259 | SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos";
260 | SWIFT_COMPILATION_MODE = wholemodule;
261 | SWIFT_OPTIMIZATION_LEVEL = "-O";
262 | TARGETED_DEVICE_FAMILY = "1,2,3";
263 | TVOS_DEPLOYMENT_TARGET = 14.0;
264 | VALIDATE_PRODUCT = YES;
265 | };
266 | name = Release;
267 | };
268 | 6F80BEE52510DF8F00192322 /* Debug */ = {
269 | isa = XCBuildConfiguration;
270 | buildSettings = {
271 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
272 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
273 | CODE_SIGN_STYLE = Automatic;
274 | DEVELOPMENT_TEAM = C859N35D2X;
275 | ENABLE_PREVIEWS = YES;
276 | INFOPLIST_FILE = Example/Info.plist;
277 | LD_RUNPATH_SEARCH_PATHS = (
278 | "$(inherited)",
279 | "@executable_path/Frameworks",
280 | );
281 | PRODUCT_BUNDLE_IDENTIFIER = ch.defagos.Example;
282 | PRODUCT_NAME = "$(TARGET_NAME)";
283 | SWIFT_VERSION = 5.0;
284 | };
285 | name = Debug;
286 | };
287 | 6F80BEE62510DF8F00192322 /* Release */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
291 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
292 | CODE_SIGN_STYLE = Automatic;
293 | DEVELOPMENT_TEAM = C859N35D2X;
294 | ENABLE_PREVIEWS = YES;
295 | INFOPLIST_FILE = Example/Info.plist;
296 | LD_RUNPATH_SEARCH_PATHS = (
297 | "$(inherited)",
298 | "@executable_path/Frameworks",
299 | );
300 | PRODUCT_BUNDLE_IDENTIFIER = ch.defagos.Example;
301 | PRODUCT_NAME = "$(TARGET_NAME)";
302 | SWIFT_VERSION = 5.0;
303 | };
304 | name = Release;
305 | };
306 | /* End XCBuildConfiguration section */
307 |
308 | /* Begin XCConfigurationList section */
309 | 6F80BED02510DF8D00192322 /* Build configuration list for PBXProject "Example" */ = {
310 | isa = XCConfigurationList;
311 | buildConfigurations = (
312 | 6F80BEE22510DF8F00192322 /* Debug */,
313 | 6F80BEE32510DF8F00192322 /* Release */,
314 | );
315 | defaultConfigurationIsVisible = 0;
316 | defaultConfigurationName = Release;
317 | };
318 | 6F80BEE42510DF8F00192322 /* Build configuration list for PBXNativeTarget "Example" */ = {
319 | isa = XCConfigurationList;
320 | buildConfigurations = (
321 | 6F80BEE52510DF8F00192322 /* Debug */,
322 | 6F80BEE62510DF8F00192322 /* Release */,
323 | );
324 | defaultConfigurationIsVisible = 0;
325 | defaultConfigurationName = Release;
326 | };
327 | /* End XCConfigurationList section */
328 |
329 | /* Begin XCSwiftPackageProductDependency section */
330 | 6F80BEEA2510E22F00192322 /* SwiftUICollection */ = {
331 | isa = XCSwiftPackageProductDependency;
332 | productName = SwiftUICollection;
333 | };
334 | /* End XCSwiftPackageProductDependency section */
335 | };
336 | rootObject = 6F80BECD2510DF8D00192322 /* Project object */;
337 | }
338 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/ExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Samuel Défago. All rights reserved.
3 | //
4 | // License information is available from the LICENSE file.
5 | //
6 |
7 | import SwiftUI
8 |
9 | @main
10 | struct ExampleApp: App {
11 | var body: some Scene {
12 | WindowGroup {
13 | Shelf()
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Example/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Example/Example/Shelf.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Samuel Défago. All rights reserved.
3 | //
4 | // License information is available from the LICENSE file.
5 | //
6 |
7 | import SwiftUI
8 | import SwiftUICollection
9 |
10 | extension Button {
11 | func cardButtonStyle() -> some View {
12 | #if os(tvOS)
13 | return self.buttonStyle(CardButtonStyle())
14 | #else
15 | return self
16 | #endif
17 | }
18 | }
19 |
20 | struct Shelf: View {
21 | typealias Row = CollectionRow
22 |
23 | @State var rows = Self.shuffledRows()
24 |
25 | private static func shuffledRows() -> [Row] {
26 | var rows = [Row]()
27 | for i in (0..<40).shuffled() {
28 | rows.append(Row(section: i, items: (0..<20).map { "\(i), \($0)" }))
29 | }
30 | return rows
31 | }
32 |
33 | private func shuffle() {
34 | rows = Self.shuffledRows()
35 | }
36 |
37 | var body: some View {
38 | CollectionView(rows: rows) { sectionIndex, layoutEnvironment in
39 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
40 | let item = NSCollectionLayoutItem(layoutSize: itemSize)
41 |
42 | let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(320), heightDimension: .absolute(180))
43 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
44 |
45 | let header = NSCollectionLayoutBoundarySupplementaryItem(
46 | layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(44)),
47 | elementKind: UICollectionView.elementKindSectionHeader,
48 | alignment: .topLeading
49 | )
50 |
51 | let section = NSCollectionLayoutSection(group: group)
52 | section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)
53 | section.interGroupSpacing = 40
54 | section.orthogonalScrollingBehavior = .continuous
55 | section.boundarySupplementaryItems = [header]
56 | return section
57 | } cell: { indexPath, item in
58 | GeometryReader { geometry in
59 | Button(action: shuffle) {
60 | Text(item)
61 | .foregroundColor(.black)
62 | .frame(width: geometry.size.width, height: geometry.size.height)
63 | .background(Color.blue)
64 | }
65 | .cardButtonStyle()
66 | }
67 | } supplementaryView: { kind, indexPath in
68 | Text("Section \(indexPath.section)")
69 | }
70 | .frame(maxWidth: .infinity, maxHeight: .infinity)
71 | .ignoresSafeArea(.all)
72 | }
73 | }
74 |
75 | struct Shelf_Previews: PreviewProvider {
76 | static var previews: some View {
77 | Shelf()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020, Samuel Défago
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.2
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SwiftUICollection",
7 | platforms: [
8 | .iOS(.v13),
9 | .tvOS(.v13)
10 | ],
11 | products: [
12 | .library(
13 | name: "SwiftUICollection",
14 | targets: ["SwiftUICollection"]
15 | )
16 | ],
17 | targets: [
18 | .target(
19 | name: "SwiftUICollection"
20 | )
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Building a Collection For SwiftUI - Sample Code
2 |
3 | This is the sample code for my [_Building a Collection For SwiftUI_](http://defagos.github.io/swiftui_collection_intro) article series.
4 |
5 | It contains:
6 |
7 | - A Swift package with a collection for SwiftUI.
8 | - A sample project.
9 |
10 | This collection intends to solve performance issues associated with SwiftUI stack and scroll view nesting, especially on tvOS. It achieves this results by wrapping `UICollectionView` internally.
11 |
12 | ## Purpose
13 |
14 | This code is a companion to the aforementioned article. It should not be used as a library. The Swift package is intended for experimental use in custom projects.
15 |
16 | ## Requirements
17 |
18 | This project must be compiled with Xcode 12.
19 |
20 | ## Compatibility
21 |
22 | The package is compatible with iOS and tvOS 13 and above. The example project runs on iOS and tvOS 14 and above.
23 |
24 | ## License
25 |
26 | See the [LICENSE](../LICENSE) file for more information.
--------------------------------------------------------------------------------
/Sources/SwiftUICollection/CollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) Samuel Défago. All rights reserved.
3 | //
4 | // License information is available from the LICENSE file.
5 | //
6 |
7 | import SwiftUI
8 |
9 | extension UIHostingController {
10 | convenience public init(rootView: Content, ignoreSafeArea: Bool) {
11 | self.init(rootView: rootView)
12 |
13 | if ignoreSafeArea {
14 | disableSafeArea()
15 | }
16 | }
17 |
18 | func disableSafeArea() {
19 | guard let viewClass = object_getClass(view) else { return }
20 |
21 | let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
22 | if let viewSubclass = NSClassFromString(viewSubclassName) {
23 | object_setClass(view, viewSubclass)
24 | }
25 | else {
26 | guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
27 | guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
28 |
29 | if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
30 | let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
31 | return .zero
32 | }
33 | class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
34 | }
35 |
36 | objc_registerClassPair(viewSubclass)
37 | object_setClass(view, viewSubclass)
38 | }
39 | }
40 | }
41 |
42 | public struct CollectionRow: Hashable {
43 | let section: Section
44 | let items: [Item]
45 |
46 | public init(section: Section, items: [Item]) {
47 | self.section = section
48 | self.items = items
49 | }
50 | }
51 |
52 | public struct CollectionView: UIViewRepresentable {
53 | private class HostCell: UICollectionViewCell {
54 | private var hostController: UIHostingController?
55 |
56 | override func prepareForReuse() {
57 | if let hostView = hostController?.view {
58 | hostView.removeFromSuperview()
59 | }
60 | hostController = nil
61 | }
62 |
63 | var hostedCell: Cell? {
64 | willSet {
65 | guard let view = newValue else { return }
66 | hostController = UIHostingController(rootView: view, ignoreSafeArea: true)
67 | if let hostView = hostController?.view {
68 | hostView.frame = contentView.bounds
69 | hostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
70 | contentView.addSubview(hostView)
71 | }
72 | }
73 | }
74 | }
75 |
76 | private class HostSupplementaryView: UICollectionReusableView {
77 | private var hostController: UIHostingController?
78 |
79 | override func prepareForReuse() {
80 | if let hostView = hostController?.view {
81 | hostView.removeFromSuperview()
82 | }
83 | hostController = nil
84 | }
85 |
86 | var hostedSupplementaryView: SupplementaryView? {
87 | willSet {
88 | guard let view = newValue else { return }
89 | hostController = UIHostingController(rootView: view, ignoreSafeArea: true)
90 | if let hostView = hostController?.view {
91 | hostView.frame = self.bounds
92 | hostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
93 | addSubview(hostView)
94 | }
95 | }
96 | }
97 | }
98 |
99 | public class Coordinator: NSObject, UICollectionViewDelegate {
100 | fileprivate typealias DataSource = UICollectionViewDiffableDataSource
101 |
102 | fileprivate var dataSource: DataSource? = nil
103 | fileprivate var sectionLayoutProvider: ((Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection)?
104 | fileprivate var rowsHash: Int? = nil
105 | fileprivate var registeredSupplementaryViewKinds: [String] = []
106 | fileprivate var isFocusable: Bool = false
107 |
108 | public func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
109 | return isFocusable
110 | }
111 | }
112 |
113 | let rows: [CollectionRow]
114 | let sectionLayoutProvider: (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection
115 | let cell: (IndexPath, Item) -> Cell
116 | let supplementaryView: (String, IndexPath) -> SupplementaryView
117 |
118 | public init(rows: [CollectionRow],
119 | sectionLayoutProvider: @escaping (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection,
120 | @ViewBuilder cell: @escaping (IndexPath, Item) -> Cell,
121 | @ViewBuilder supplementaryView: @escaping (String, IndexPath) -> SupplementaryView) {
122 | self.rows = rows
123 | self.sectionLayoutProvider = sectionLayoutProvider
124 | self.cell = cell
125 | self.supplementaryView = supplementaryView
126 | }
127 |
128 | private func layout(context: Context) -> UICollectionViewLayout {
129 | return UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
130 | return context.coordinator.sectionLayoutProvider!(sectionIndex, layoutEnvironment)
131 | }
132 | }
133 |
134 | private func snapshot() -> NSDiffableDataSourceSnapshot {
135 | var snapshot = NSDiffableDataSourceSnapshot()
136 | for row in rows {
137 | snapshot.appendSections([row.section])
138 | snapshot.appendItems(row.items, toSection: row.section)
139 | }
140 | return snapshot
141 | }
142 |
143 | private func reloadData(in collectionView: UICollectionView, context: Context, animated: Bool = false) {
144 | let coordinator = context.coordinator
145 | coordinator.sectionLayoutProvider = self.sectionLayoutProvider
146 |
147 | guard let dataSource = coordinator.dataSource else { return }
148 |
149 | let rowsHash = rows.hashValue
150 | if coordinator.rowsHash != rowsHash {
151 | dataSource.apply(snapshot(), animatingDifferences: animated) {
152 | coordinator.isFocusable = true
153 | collectionView.setNeedsFocusUpdate()
154 | collectionView.updateFocusIfNeeded()
155 | coordinator.isFocusable = false
156 | }
157 | coordinator.rowsHash = rowsHash
158 | }
159 | }
160 |
161 | public func makeCoordinator() -> Coordinator {
162 | return Coordinator()
163 | }
164 |
165 | public func makeUIView(context: Context) -> UICollectionView {
166 | let cellIdentifier = "hostCell"
167 | let supplementaryViewIdentifier = "hostSupplementaryView"
168 |
169 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout(context: context))
170 | collectionView.delegate = context.coordinator
171 | collectionView.register(HostCell.self, forCellWithReuseIdentifier: cellIdentifier)
172 |
173 | let dataSource = Coordinator.DataSource(collectionView: collectionView) { collectionView, indexPath, item in
174 | let hostCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? HostCell
175 | hostCell?.hostedCell = cell(indexPath, item)
176 | return hostCell
177 | }
178 | context.coordinator.dataSource = dataSource
179 |
180 | dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
181 | let coordinator = context.coordinator
182 | if !coordinator.registeredSupplementaryViewKinds.contains(kind) {
183 | collectionView.register(HostSupplementaryView.self, forSupplementaryViewOfKind: kind, withReuseIdentifier: supplementaryViewIdentifier)
184 | coordinator.registeredSupplementaryViewKinds.append(kind)
185 | }
186 |
187 | guard let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: supplementaryViewIdentifier, for: indexPath) as? HostSupplementaryView else { return nil }
188 | view.hostedSupplementaryView = supplementaryView(kind, indexPath)
189 | return view
190 | }
191 |
192 | reloadData(in: collectionView, context: context)
193 | return collectionView
194 | }
195 |
196 | public func updateUIView(_ uiView: UICollectionView, context: Context) {
197 | reloadData(in: uiView, context: context, animated: true)
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
|