From 4ed8a049efa9e4a8d52abe1dc2cdf3783275a129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20De=CC=81fago?= Date: Tue, 15 Sep 2020 13:54:16 +0200 Subject: [PATCH] Add collection view source code --- .gitignore | 5 + .../contents.xcworkspacedata | 7 + .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Example/Example.xcodeproj/project.pbxproj | 335 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Example/Example/ExampleApp.swift | 16 + Example/Example/Info.plist | 50 +++ Example/Example/Shelf.swift | 53 +++ LICENSE | 7 + Package.swift | 22 ++ README.md | 3 + .../SwiftUICollection/CollectionView.swift | 151 ++++++++ 14 files changed, 682 insertions(+) create mode 100644 .gitignore create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/Example.xcodeproj/project.pbxproj create mode 100644 Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/Example/ExampleApp.swift create mode 100644 Example/Example/Info.plist create mode 100644 Example/Example/Shelf.swift create mode 100644 LICENSE create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/SwiftUICollection/CollectionView.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..e1a9a84 --- /dev/null +++ b/Example/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0118e8c --- /dev/null +++ b/Example/Example.xcodeproj/project.pbxproj @@ -0,0 +1,335 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 6F80BED92510DF8D00192322 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80BED82510DF8D00192322 /* ExampleApp.swift */; }; + 6F80BEE82510E1F700192322 /* Shelf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F80BEE72510E1F700192322 /* Shelf.swift */; }; + 6F80BEEB2510E22F00192322 /* SwiftUICollection in Frameworks */ = {isa = PBXBuildFile; productRef = 6F80BEEA2510E22F00192322 /* SwiftUICollection */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 6F80BED52510DF8D00192322 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F80BED82510DF8D00192322 /* ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; + 6F80BEE12510DF8F00192322 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6F80BEE72510E1F700192322 /* Shelf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shelf.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6F80BED22510DF8D00192322 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F80BEEB2510E22F00192322 /* SwiftUICollection in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6F80BECC2510DF8D00192322 = { + isa = PBXGroup; + children = ( + 6F80BED72510DF8D00192322 /* Example */, + 6F80BED62510DF8D00192322 /* Products */, + 6F80BEE92510E22F00192322 /* Frameworks */, + ); + sourceTree = ""; + }; + 6F80BED62510DF8D00192322 /* Products */ = { + isa = PBXGroup; + children = ( + 6F80BED52510DF8D00192322 /* Example.app */, + ); + name = Products; + sourceTree = ""; + }; + 6F80BED72510DF8D00192322 /* Example */ = { + isa = PBXGroup; + children = ( + 6F80BEE72510E1F700192322 /* Shelf.swift */, + 6F80BED82510DF8D00192322 /* ExampleApp.swift */, + 6F80BEE12510DF8F00192322 /* Info.plist */, + ); + path = Example; + sourceTree = ""; + }; + 6F80BEE92510E22F00192322 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6F80BED42510DF8D00192322 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6F80BEE42510DF8F00192322 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 6F80BED12510DF8D00192322 /* Sources */, + 6F80BED22510DF8D00192322 /* Frameworks */, + 6F80BED32510DF8D00192322 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + packageProductDependencies = ( + 6F80BEEA2510E22F00192322 /* SwiftUICollection */, + ); + productName = Example; + productReference = 6F80BED52510DF8D00192322 /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6F80BECD2510DF8D00192322 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1200; + LastUpgradeCheck = 1200; + TargetAttributes = { + 6F80BED42510DF8D00192322 = { + CreatedOnToolsVersion = 12.0; + }; + }; + }; + buildConfigurationList = 6F80BED02510DF8D00192322 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6F80BECC2510DF8D00192322; + productRefGroup = 6F80BED62510DF8D00192322 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6F80BED42510DF8D00192322 /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6F80BED32510DF8D00192322 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6F80BED12510DF8D00192322 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F80BEE82510E1F700192322 /* Shelf.swift in Sources */, + 6F80BED92510DF8D00192322 /* ExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6F80BEE22510DF8F00192322 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + }; + name = Debug; + }; + 6F80BEE32510DF8F00192322 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos appletvsimulator appletvos"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2,3"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6F80BEE52510DF8F00192322 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ch.defagos.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 6F80BEE62510DF8F00192322 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"Example/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ch.defagos.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6F80BED02510DF8D00192322 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6F80BEE22510DF8F00192322 /* Debug */, + 6F80BEE32510DF8F00192322 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6F80BEE42510DF8F00192322 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6F80BEE52510DF8F00192322 /* Debug */, + 6F80BEE62510DF8F00192322 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6F80BEEA2510E22F00192322 /* SwiftUICollection */ = { + isa = XCSwiftPackageProductDependency; + productName = SwiftUICollection; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 6F80BECD2510DF8D00192322 /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example/ExampleApp.swift b/Example/Example/ExampleApp.swift new file mode 100644 index 0000000..c1c2952 --- /dev/null +++ b/Example/Example/ExampleApp.swift @@ -0,0 +1,16 @@ +// +// Copyright (c) Samuel Défago. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import SwiftUI + +@main +struct ExampleApp: App { + var body: some Scene { + WindowGroup { + Shelf() + } + } +} diff --git a/Example/Example/Info.plist b/Example/Example/Info.plist new file mode 100644 index 0000000..efc211a --- /dev/null +++ b/Example/Example/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/Example/Shelf.swift b/Example/Example/Shelf.swift new file mode 100644 index 0000000..cfbed9b --- /dev/null +++ b/Example/Example/Shelf.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) Samuel Défago. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import SwiftUI +import SwiftUICollection + +struct Shelf: View { + typealias Row = CollectionRow + + var rows: [Row] = { + var rows = [Row]() + for i in 0..<100 { + rows.append(Row(section: i, items: (0..<40).map { "\(i), \($0)" })) + } + return rows + }() + + var body: some View { + CollectionView(rows: rows) { sectionIndex, layoutEnvironment in + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + + let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(320), heightDimension: .absolute(180)) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) + + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0) + section.interGroupSpacing = 40 + section.orthogonalScrollingBehavior = .continuous + return section + } cell: { indexPath, item in + GeometryReader { geometry in + Button(action: {}) { + Text(item) + .frame(width: geometry.size.width, height: geometry.size.height) + .background(Color.blue) + } + .buttonStyle(CardButtonStyle()) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea(.all) + } +} + +struct Shelf_Previews: PreviewProvider { + static var previews: some View { + Shelf() + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..11a76f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2020, Samuel Défago + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..05aba8f --- /dev/null +++ b/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "SwiftUICollection", + platforms: [ + .iOS(.v13), + .tvOS(.v13) + ], + products: [ + .library( + name: "SwiftUICollection", + targets: ["SwiftUICollection"] + ) + ], + targets: [ + .target( + name: "SwiftUICollection" + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3c39a6 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# SwiftUICollection + +A description of this package. diff --git a/Sources/SwiftUICollection/CollectionView.swift b/Sources/SwiftUICollection/CollectionView.swift new file mode 100644 index 0000000..90465fe --- /dev/null +++ b/Sources/SwiftUICollection/CollectionView.swift @@ -0,0 +1,151 @@ +// +// Copyright (c) Samuel Défago. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import SwiftUI + +extension UIHostingController { + convenience public init(rootView: Content, ignoreSafeArea: Bool) { + self.init(rootView: rootView) + + if ignoreSafeArea { + disableSafeArea() + } + } + + func disableSafeArea() { + guard let viewClass = object_getClass(view) else { return } + + let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") + if let viewSubclass = NSClassFromString(viewSubclassName) { + object_setClass(view, viewSubclass) + } + else { + guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } + guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } + + if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { + let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in + return .zero + } + class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) + } + + objc_registerClassPair(viewSubclass) + object_setClass(view, viewSubclass) + } + } +} + +public struct CollectionRow: Hashable { + let section: Section + let items: [Item] + + public init(section: Section, items: [Item]) { + self.section = section + self.items = items + } +} + +public struct CollectionView: UIViewRepresentable { + private class HostCell: UICollectionViewCell { + private var hostController: UIHostingController? + + override func prepareForReuse() { + if let hostView = hostController?.view { + hostView.removeFromSuperview() + } + hostController = nil + } + + var hostedCell: Cell? { + willSet { + guard let view = newValue else { return } + hostController = UIHostingController(rootView: view, ignoreSafeArea: true) + if let hostView = hostController?.view { + hostView.frame = contentView.bounds + hostView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + contentView.addSubview(hostView) + } + } + } + + override var canBecomeFocused: Bool { + return false + } + } + + public class Coordinator { + fileprivate typealias DataSource = UICollectionViewDiffableDataSource + + fileprivate var dataSource: DataSource? = nil + fileprivate var sectionLayoutProvider: ((Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection)? + fileprivate var rowsHash: Int? = nil + } + + let rows: [CollectionRow] + let sectionLayoutProvider: (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection + let cell: (IndexPath, Item) -> Cell + + public init(rows: [CollectionRow], + sectionLayoutProvider: @escaping (Int, NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection, + @ViewBuilder cell: @escaping (IndexPath, Item) -> Cell) { + self.rows = rows + self.sectionLayoutProvider = sectionLayoutProvider + self.cell = cell + } + + private func layout(context: Context) -> UICollectionViewLayout { + return UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in + return context.coordinator.sectionLayoutProvider!(sectionIndex, layoutEnvironment) + } + } + + private func snapshot() -> NSDiffableDataSourceSnapshot { + var snapshot = NSDiffableDataSourceSnapshot() + for row in rows { + snapshot.appendSections([row.section]) + snapshot.appendItems(row.items, toSection: row.section) + } + return snapshot + } + + private func reloadData(context: Context, animated: Bool = false) { + let coordinator = context.coordinator + coordinator.sectionLayoutProvider = self.sectionLayoutProvider + + guard let dataSource = coordinator.dataSource else { return } + + let rowsHash = rows.hashValue + if coordinator.rowsHash != rowsHash { + dataSource.apply(snapshot(), animatingDifferences: animated) + coordinator.rowsHash = rowsHash + } + } + + public func makeCoordinator() -> Coordinator { + return Coordinator() + } + + public func makeUIView(context: Context) -> some UIView { + let cellIdentifier = "hostCell" + + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout(context: context)) + collectionView.register(HostCell.self, forCellWithReuseIdentifier: cellIdentifier) + + context.coordinator.dataSource = Coordinator.DataSource(collectionView: collectionView) { collectionView, indexPath, item in + let hostCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? HostCell + hostCell?.hostedCell = cell(indexPath, item) + return hostCell + } + + reloadData(context: context) + return collectionView + } + + public func updateUIView(_ uiView: UIViewType, context: Context) { + reloadData(context: context, animated: true) + } +}