├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── build.yml ├── .gitignore ├── .swiftformat ├── ASCollectionView-SwiftUI.podspec ├── Demo ├── ASCollectionViewDemo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ ├── xcshareddata │ │ └── xcschemes │ │ │ ├── ASCollectionViewDemo-ReleaseConfig.xcscheme │ │ │ └── ASCollectionViewDemo.xcscheme │ └── xcuserdata │ │ └── tobybrennan.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── ASCollectionViewDemo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── MainView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── Screens │ │ ├── AdjustableLayout │ │ │ └── AdjustableGridScreen.swift │ │ ├── AppStore │ │ │ ├── App.swift │ │ │ ├── AppStoreScreen.swift │ │ │ └── AppViews.swift │ │ ├── InstaFeed │ │ │ ├── InstaFeedScreen.swift │ │ │ ├── PostView.swift │ │ │ └── StoryView.swift │ │ ├── MagazineLayout │ │ │ ├── CustomDelegate.swift │ │ │ └── MagazineLayoutScreen.swift │ │ ├── PhotoGrid │ │ │ └── PhotoGridScreen.swift │ │ ├── Reminders │ │ │ ├── GroupLarge.swift │ │ │ ├── GroupModel.swift │ │ │ ├── GroupSmall.swift │ │ │ └── RemindersScreen.swift │ │ ├── TableViewDragAndDrop │ │ │ └── TableViewDragAndDropScreen.swift │ │ ├── Tags │ │ │ ├── TagStore.swift │ │ │ └── TagsScreen.swift │ │ └── Waterfall │ │ │ └── WaterfallScreen.swift │ ├── SharedModels │ │ └── Post.swift │ └── Support │ │ ├── ASCache.swift │ │ ├── ASRemoteImageManager.swift │ │ ├── ASRemoteImageView.swift │ │ ├── LoremSwiftum.swift │ │ └── OnChange.swift ├── BuildTools │ ├── BuildTools.swift │ ├── Package.resolved │ └── Package.swift └── README code content.swift ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── ASCollectionView │ ├── ASCellContext.swift │ ├── ASCollectionView+Initialisers.swift │ ├── ASCollectionView+Modifiers.swift │ ├── ASDragDropConfig+Public.swift │ ├── ASSection+Initialisers.swift │ ├── ASSection+Modifiers.swift │ ├── ASTableView+Initialisers.swift │ ├── ASTableView+Modifiers.swift │ ├── Cells │ ├── ASCollectionViewCell.swift │ ├── ASCollectionViewDecoration.swift │ ├── ASCollectionViewSupplementaryView.swift │ ├── ASSupplementaryCellID.swift │ ├── ASTableViewCell.swift │ └── ASTableViewSupplementaryView.swift │ ├── Config │ ├── ASDragDropConfig.swift │ └── ClosureTypeAliases.swift │ ├── Datasource │ ├── ASDiffableDataSource.swift │ ├── ASDiffableDataSourceCollectionView.swift │ └── ASDiffableDataSourceTableView.swift │ ├── Delegate │ └── ASCollectionViewDelegate.swift │ ├── Environment │ └── EnvironmentKeys.swift │ ├── FunctionBuilders │ ├── SectionArrayBuilder.swift │ └── ViewArrayBuilder.swift │ ├── Implementation │ ├── ASCollectionView.swift │ ├── ASHostingController.swift │ ├── ASSection.swift │ ├── ASSectionDataSource.swift │ └── ASTableView.swift │ ├── Layout │ ├── ASCollectionViewLayout.swift │ └── ASWaterfallLayout.swift │ ├── Support │ ├── ASIndexedDictionary.swift │ ├── ASOptionalSize.swift │ ├── ASPriorityCache.swift │ ├── ASSelfSizingSettings.swift │ ├── Binding+Sequence.swift │ ├── GlobalConvenienceFunctions.swift │ ├── RandomAccessCollection+Safe.swift │ └── ShrinkToFitWrapper.swift │ ├── UIKit │ ├── AS_UICollectionView.swift │ └── AS_UITableView.swift │ └── UIKitExtensions │ ├── UICollectionView+Convenience.swift │ ├── UIScrollView+Convenience.swift │ └── UIView+Convenience.swift └── readmeAssets ├── SampleUsage.swift ├── demo1.jpeg ├── demo2.jpeg ├── demo3.jpeg ├── demo4.jpeg ├── demo5.jpeg ├── demo6.jpeg └── demo7.jpeg /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | github: apptekstudios 13 | custom: ['https://www.buymeacoffee.com/tobeasbrennan'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behaviour: 15 | 16 | 17 | **Expected behaviour** 18 | A description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Xcode Version:** 24 | - 25 | 26 | **Simulator, Device, Both?** 27 | - Where is the problem occuring 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: set -o pipefail; xcodebuild -project Demo/ASCollectionViewDemo.xcodeproj -scheme ASCollectionViewDemo -destination platform\=iOS\ Simulator,OS\=14.4,name\=iPhone\ 11 build | xcpretty 21 | - name: Lint podspec 22 | run: pod lib lint 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Demo/BuildTools/.build 2 | 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # 49 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 50 | # hence it is not needed unless you have added a package configuration file to your project 51 | .swiftpm 52 | 53 | .build/ 54 | 55 | # CocoaPods 56 | # 57 | # We recommend against adding the Pods directory to your .gitignore. However 58 | # you should judge for yourself, the pros and cons are mentioned at: 59 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 60 | # 61 | # Pods/ 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --symlinks ignore 4 | --swiftversion 5.2 5 | 6 | --exclude Package.swift 7 | 8 | # format options 9 | 10 | --header "ASCollectionView. Created by Apptek Studios 2019" 11 | --allman true 12 | --indent tabs 13 | --tabwidth 4 14 | --closingparen same-line 15 | --commas inline 16 | --comments indent 17 | --decimalgrouping 3,5 18 | --exponentcase lowercase 19 | --exponentgrouping disabled 20 | --fractiongrouping disabled 21 | --ifdef outdent 22 | --importgrouping testable-top 23 | --wraparguments before-first 24 | --wrapcollections after-first 25 | 26 | --disable unusedArguments 27 | -------------------------------------------------------------------------------- /ASCollectionView-SwiftUI.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = 'ASCollectionView-SwiftUI' 4 | s.version = '2.0.0' 5 | s.summary = 'A SwiftUI collection view with support for custom layouts, preloading, and more. ' 6 | 7 | s.description = <<-DESC 8 | A SwiftUI implementation of UICollectionView & UITableView. Here's some of its useful features: 9 | - supports preloading and onAppear/onDisappear. 10 | - supports cell selection, with automatic support for SwiftUI editing mode. 11 | - supports autosizing of cells. 12 | - supports the new UICollectionViewCompositionalLayout, and any other UICollectionViewLayout 13 | - supports removing separators for ASTableView. 14 | DESC 15 | 16 | s.homepage = 'https://github.com/apptekstudios/ASCollectionView' 17 | s.license = { :type => 'MIT', :file => 'LICENSE' } 18 | s.author = { 'apptekstudios' => '' } 19 | s.source = { :git => 'https://github.com/apptekstudios/ASCollectionView.git', :tag => s.version.to_s } 20 | 21 | s.ios.deployment_target = '11.0' 22 | s.swift_versions = '5.3' 23 | s.source_files = 'Sources/ASCollectionView/**/*' 24 | s.dependency 'DifferenceKit', '~> 1.1' 25 | end 26 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DifferenceKit", 6 | "repositoryURL": "https://github.com/ra1028/DifferenceKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "14c66681e12a38b81045f44c6c29724a0d4b0e72", 10 | "version": "1.1.5" 11 | } 12 | }, 13 | { 14 | "package": "MagazineLayout", 15 | "repositoryURL": "https://github.com/airbnb/MagazineLayout", 16 | "state": { 17 | "branch": "master", 18 | "revision": "4a5eff2203ad8d8c7e14ea1b283b64f9320752a9", 19 | "version": null 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/xcshareddata/xcschemes/ASCollectionViewDemo-ReleaseConfig.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/xcshareddata/xcschemes/ASCollectionViewDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo.xcodeproj/xcuserdata/tobybrennan.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ASCollectionViewDemo-ReleaseConfig.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | ASCollectionViewDemo.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | DifferenceKit (Playground) 1.xcscheme 18 | 19 | isShown 20 | 21 | orderHint 22 | 5 23 | 24 | DifferenceKit (Playground) 2.xcscheme 25 | 26 | isShown 27 | 28 | orderHint 29 | 6 30 | 31 | DifferenceKit (Playground).xcscheme 32 | 33 | isShown 34 | 35 | orderHint 36 | 4 37 | 38 | 39 | SuppressBuildableAutocreation 40 | 41 | B86C6F12234B078600522AEF 42 | 43 | primary 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate 7 | { 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool 9 | { 10 | // Override point for customization after application launch. 11 | true 12 | } 13 | 14 | // MARK: UISceneSession Lifecycle 15 | 16 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration 17 | { 18 | // Called when a new scene session is being created. 19 | // Use this method to select a configuration to create the new scene with. 20 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 21 | } 22 | 23 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) 24 | { 25 | // Called when the user discards a scene session. 26 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 27 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/MainView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | struct MainView: View 6 | { 7 | var body: some View 8 | { 9 | NavigationView 10 | { 11 | List 12 | { 13 | Section(header: Text("Example screens")) 14 | { 15 | NavigationLink(destination: PhotoGridScreen()) 16 | { 17 | Image(systemName: "1.square.fill") 18 | Text("Photo grid (with edit mode, selection)") 19 | } 20 | NavigationLink(destination: AppStoreScreen()) 21 | { 22 | Image(systemName: "2.square.fill") 23 | Text("App Store Layout") 24 | } 25 | NavigationLink(destination: TagsScreen()) 26 | { 27 | Image(systemName: "3.square.fill") 28 | Text("Tags Flow Layout") 29 | } 30 | NavigationLink(destination: RemindersScreen()) 31 | { 32 | Image(systemName: "4.square.fill") 33 | Text("Reminders Layout") 34 | } 35 | NavigationLink(destination: WaterfallScreen()) 36 | { 37 | Image(systemName: "5.square.fill") 38 | Text("Waterfall Layout") 39 | } 40 | NavigationLink(destination: InstaFeedScreen()) 41 | { 42 | Image(systemName: "6.square.fill") 43 | Text("Insta Feed (table view)") 44 | } 45 | NavigationLink(destination: MagazineLayoutScreen()) 46 | { 47 | Image(systemName: "7.square.fill") 48 | Text("Magazine Layout (with context menu)") 49 | } 50 | NavigationLink(destination: AdjustableGridScreen()) 51 | { 52 | Image(systemName: "8.square.fill") 53 | Text("Adjustable layout") 54 | } 55 | NavigationLink(destination: TableViewDragAndDropScreen()) 56 | { 57 | Image(systemName: "9.square.fill") 58 | Text("Multiple TableView drag&drop") 59 | } 60 | } 61 | } 62 | .navigationBarTitle("Demo App") 63 | } 64 | .navigationViewStyle(StackNavigationViewStyle()) 65 | } 66 | } 67 | 68 | struct MainView_Previews: PreviewProvider 69 | { 70 | static var previews: some View 71 | { 72 | MainView() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | import UIKit 5 | 6 | class SceneDelegate: UIResponder, UIWindowSceneDelegate 7 | { 8 | var window: UIWindow? 9 | 10 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) 11 | { 12 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 13 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 14 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 15 | 16 | // Create the SwiftUI view that provides the window contents. 17 | let contentView = MainView() 18 | 19 | // Use a UIHostingController as window root view controller. 20 | if let windowScene = scene as? UIWindowScene 21 | { 22 | let window = UIWindow(windowScene: windowScene) 23 | window.rootViewController = UIHostingController(rootView: contentView) 24 | self.window = window 25 | window.makeKeyAndVisible() 26 | } 27 | } 28 | 29 | func sceneDidDisconnect(_ scene: UIScene) 30 | { 31 | // Called as the scene is being released by the system. 32 | // This occurs shortly after the scene enters the background, or when its session is discarded. 33 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 34 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 35 | } 36 | 37 | func sceneDidBecomeActive(_ scene: UIScene) 38 | { 39 | // Called when the scene has moved from an inactive state to an active state. 40 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 41 | } 42 | 43 | func sceneWillResignActive(_ scene: UIScene) 44 | { 45 | // Called when the scene will move from an active state to an inactive state. 46 | // This may occur due to temporary interruptions (ex. an incoming phone call). 47 | } 48 | 49 | func sceneWillEnterForeground(_ scene: UIScene) 50 | { 51 | // Called as the scene transitions from the background to the foreground. 52 | // Use this method to undo the changes made on entering the background. 53 | } 54 | 55 | func sceneDidEnterBackground(_ scene: UIScene) 56 | { 57 | // Called as the scene transitions from the foreground to the background. 58 | // Use this method to save data, release shared resources, and store enough scene-specific state information 59 | // to restore the scene back to its current state. 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/AdjustableLayout/AdjustableGridScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | import UIKit 6 | 7 | class LayoutState: ObservableObject 8 | { 9 | @Published 10 | var numberOfColumns: Int = 3 11 | 12 | @Published 13 | var itemInset: Int = 0 14 | } 15 | 16 | struct AdjustableGridScreen: View 17 | { 18 | @ObservedObject var layoutState = LayoutState() 19 | @State var showConfig: Bool = true 20 | @State var data: [Post] = DataSource.postsForGridSection(1, number: 1000) 21 | 22 | typealias SectionID = Int 23 | 24 | var section: ASCollectionViewSection 25 | { 26 | ASCollectionViewSection( 27 | id: 0, 28 | data: data, 29 | onCellEvent: onCellEvent) 30 | { item, _ in 31 | ZStack(alignment: .bottomTrailing) 32 | { 33 | GeometryReader 34 | { geom in 35 | ASRemoteImageView(item.url) 36 | .aspectRatio(1, contentMode: .fill) 37 | .frame(width: geom.size.width, height: geom.size.height) 38 | .clipped() 39 | } 40 | 41 | Text("\(item.offset)") 42 | .font(.headline) 43 | .bold() 44 | .padding(2) 45 | .background(Color(.systemBackground).opacity(0.5)) 46 | .cornerRadius(4) 47 | .padding(10) 48 | } 49 | } 50 | } 51 | 52 | var config: some View 53 | { 54 | VStack 55 | { 56 | Stepper("Number of columns", value: self.$layoutState.numberOfColumns, in: 1 ... 10) 57 | .padding() 58 | Stepper("Item inset", value: self.$layoutState.itemInset, in: 0 ... 5) 59 | .padding() 60 | } 61 | } 62 | 63 | var body: some View 64 | { 65 | VStack 66 | { 67 | if showConfig 68 | { 69 | config 70 | } 71 | ASCollectionView( 72 | section: section) 73 | .layout(self.layout) 74 | .shouldInvalidateLayoutOnStateChange(true) 75 | .navigationBarTitle("Adjustable Layout", displayMode: .inline) 76 | } 77 | .navigationBarItems( 78 | trailing: 79 | Button(action: { 80 | self.showConfig.toggle() 81 | }) 82 | { 83 | Text("Toggle config") 84 | }) 85 | } 86 | 87 | func onCellEvent(_ event: CellEvent) 88 | { 89 | switch event 90 | { 91 | case let .onAppear(item): 92 | ASRemoteImageManager.shared.load(item.url) 93 | case let .onDisappear(item): 94 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 95 | case let .prefetchForData(data): 96 | for item in data 97 | { 98 | ASRemoteImageManager.shared.load(item.url) 99 | } 100 | case let .cancelPrefetchForData(data): 101 | for item in data 102 | { 103 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 104 | } 105 | } 106 | } 107 | } 108 | 109 | extension AdjustableGridScreen 110 | { 111 | var layout: ASCollectionLayout 112 | { 113 | ASCollectionLayout(scrollDirection: .vertical, interSectionSpacing: 0) 114 | { 115 | ASCollectionLayoutSection 116 | { 117 | let gridBlockSize = NSCollectionLayoutDimension.fractionalWidth(1 / CGFloat(self.layoutState.numberOfColumns)) 118 | let item = NSCollectionLayoutItem( 119 | layoutSize: NSCollectionLayoutSize( 120 | widthDimension: gridBlockSize, 121 | heightDimension: .fractionalHeight(1.0))) 122 | let inset = CGFloat(self.layoutState.itemInset) 123 | item.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset) 124 | 125 | let itemsGroup = NSCollectionLayoutGroup.horizontal( 126 | layoutSize: NSCollectionLayoutSize( 127 | widthDimension: .fractionalWidth(1.0), 128 | heightDimension: gridBlockSize), 129 | subitems: [item]) 130 | 131 | let section = NSCollectionLayoutSection(group: itemsGroup) 132 | return section 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/AppStore/App.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | struct App: Identifiable 6 | { 7 | var appName: String 8 | var caption: String 9 | 10 | var randomNumberForImage: Int 11 | 12 | var featureImageURL: URL 13 | { 14 | URL(string: "https://picsum.photos/800/500?random=\(abs(randomNumberForImage))")! 15 | } 16 | 17 | var url: URL 18 | { 19 | URL(string: "https://picsum.photos/500?random=\(abs(randomNumberForImage))")! 20 | } 21 | 22 | var id: Int 23 | { 24 | randomNumberForImage.hashValue 25 | } 26 | 27 | static func randomApp(_ randomNumber: Int) -> App 28 | { 29 | App( 30 | appName: Lorem.title, 31 | caption: Lorem.caption, 32 | randomNumberForImage: randomNumber) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/AppStore/AppStoreScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct AppStoreScreen: View 7 | { 8 | @State var data: [(sectionTitle: String, apps: [App])] = (0 ... 20).map 9 | { 10 | (Lorem.title, DataSource.appsForSection($0)) 11 | } 12 | 13 | func header(withTitle title: String) -> some View 14 | { 15 | HStack 16 | { 17 | Text(title) 18 | .font(.title) 19 | Spacer() 20 | Button(action: { 21 | // 22 | }) 23 | { 24 | Text("See all") 25 | } 26 | } 27 | } 28 | 29 | var sections: [ASCollectionViewSection] 30 | { 31 | data.enumerated().map 32 | { (sectionID, sectionData) -> ASCollectionViewSection in 33 | ASCollectionViewSection( 34 | id: sectionID, 35 | data: sectionData.apps, 36 | onCellEvent: { 37 | self.onCellEvent($0, sectionID: sectionID) 38 | }) 39 | { item, _ in 40 | if sectionID == 0 41 | { 42 | AppViewFeature(app: item) 43 | } 44 | else if sectionID == 1 45 | { 46 | AppViewLarge(app: item) 47 | } 48 | else 49 | { 50 | AppViewCompact(app: item) 51 | } 52 | } 53 | .sectionHeader 54 | { 55 | self.header(withTitle: sectionData.sectionTitle) 56 | } 57 | } 58 | } 59 | 60 | var body: some View 61 | { 62 | ASCollectionView(sections: self.sections) 63 | .layout(self.layout) 64 | .contentInsets(.init(top: 10, left: 0, bottom: 10, right: 0)) 65 | .shouldAttemptToMaintainScrollPositionOnOrientationChange(maintainPosition: false) 66 | .navigationBarTitle("Apps", displayMode: .inline) 67 | .edgesIgnoringSafeArea(.all) 68 | } 69 | 70 | func onCellEvent(_ event: CellEvent, sectionID: Int) 71 | { 72 | switch event 73 | { 74 | case let .onAppear(item): 75 | switch sectionID 76 | { 77 | case 0: 78 | ASRemoteImageManager.shared.load(item.featureImageURL) 79 | default: 80 | ASRemoteImageManager.shared.load(item.url) 81 | } 82 | case let .onDisappear(item): 83 | switch sectionID 84 | { 85 | case 0: 86 | ASRemoteImageManager.shared.cancelLoad(for: item.featureImageURL) 87 | default: 88 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 89 | } 90 | case let .prefetchForData(data): 91 | for item in data 92 | { 93 | switch sectionID 94 | { 95 | case 0: 96 | ASRemoteImageManager.shared.load(item.featureImageURL) 97 | default: 98 | ASRemoteImageManager.shared.load(item.url) 99 | } 100 | } 101 | case let .cancelPrefetchForData(data): 102 | for item in data 103 | { 104 | switch sectionID 105 | { 106 | case 0: 107 | ASRemoteImageManager.shared.cancelLoad(for: item.featureImageURL) 108 | default: 109 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | extension AppStoreScreen 117 | { 118 | var layout: ASCollectionLayout 119 | { 120 | ASCollectionLayout(scrollDirection: .vertical, interSectionSpacing: 20) 121 | { sectionID in 122 | switch sectionID 123 | { 124 | case 0: 125 | return ASCollectionLayoutSection 126 | { environment in 127 | let columnsToFit = floor(environment.container.effectiveContentSize.width / 320) 128 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 129 | widthDimension: .fractionalWidth(1.0), 130 | heightDimension: .fractionalHeight(1.0))) 131 | 132 | let itemsGroup = NSCollectionLayoutGroup.vertical( 133 | layoutSize: NSCollectionLayoutSize( 134 | widthDimension: .fractionalWidth(0.8 / columnsToFit), 135 | heightDimension: .absolute(280)), 136 | subitem: item, count: 1) 137 | 138 | let section = NSCollectionLayoutSection(group: itemsGroup) 139 | section.interGroupSpacing = 20 140 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) 141 | section.orthogonalScrollingBehavior = .groupPaging 142 | section.visibleItemsInvalidationHandler = { _, _, _ in } // If this isn't defined, there is a bug in UICVCompositional Layout that will fail to update sizes of cells 143 | 144 | return section 145 | } 146 | case 1: 147 | return ASCollectionLayoutSection 148 | { environment in 149 | let columnsToFit = floor(environment.container.effectiveContentSize.width / 320) 150 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 151 | widthDimension: .fractionalWidth(1.0), 152 | heightDimension: .fractionalHeight(1.0))) 153 | 154 | let itemsGroup = NSCollectionLayoutGroup.vertical( 155 | layoutSize: NSCollectionLayoutSize( 156 | widthDimension: .fractionalWidth(1.0), 157 | heightDimension: .fractionalHeight(1.0)), 158 | subitem: item, count: 2) 159 | itemsGroup.interItemSpacing = .fixed(10) 160 | 161 | let nestedGroup = NSCollectionLayoutGroup.horizontal( 162 | layoutSize: NSCollectionLayoutSize( 163 | widthDimension: .fractionalWidth(0.9 / columnsToFit), 164 | heightDimension: .absolute(180)), 165 | subitems: [itemsGroup]) 166 | nestedGroup.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 8, bottom: 10, trailing: 8) 167 | 168 | let header = NSCollectionLayoutBoundarySupplementaryItem( 169 | layoutSize: NSCollectionLayoutSize( 170 | widthDimension: .fractionalWidth(1.0), 171 | heightDimension: .absolute(34)), 172 | elementKind: UICollectionView.elementKindSectionHeader, 173 | alignment: .top) 174 | header.contentInsets.leading = nestedGroup.contentInsets.leading 175 | header.contentInsets.trailing = nestedGroup.contentInsets.trailing 176 | 177 | let section = NSCollectionLayoutSection(group: nestedGroup) 178 | section.boundarySupplementaryItems = [header] 179 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) 180 | section.orthogonalScrollingBehavior = .groupPaging 181 | section.visibleItemsInvalidationHandler = { _, _, _ in } // If this isn't defined, there is a bug in UICVCompositional Layout that will fail to update sizes of cells 182 | 183 | return section 184 | } 185 | default: 186 | return ASCollectionLayoutSection 187 | { environment in 188 | let columnsToFit = floor(environment.container.effectiveContentSize.width / 320) 189 | let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize( 190 | widthDimension: .fractionalWidth(1.0), 191 | heightDimension: .fractionalHeight(1.0))) 192 | 193 | let itemsGroup = NSCollectionLayoutGroup.vertical( 194 | layoutSize: NSCollectionLayoutSize( 195 | widthDimension: .fractionalWidth(1.0), 196 | heightDimension: .fractionalHeight(1.0)), 197 | subitem: item, count: 3) 198 | itemsGroup.interItemSpacing = .fixed(10) 199 | 200 | let nestedGroup = NSCollectionLayoutGroup.horizontal( 201 | layoutSize: NSCollectionLayoutSize( 202 | widthDimension: .fractionalWidth(0.9 / columnsToFit), 203 | heightDimension: .absolute(240)), 204 | subitems: [itemsGroup]) 205 | nestedGroup.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 8, bottom: 10, trailing: 8) 206 | 207 | let header = NSCollectionLayoutBoundarySupplementaryItem( 208 | layoutSize: NSCollectionLayoutSize( 209 | widthDimension: .fractionalWidth(1.0), 210 | heightDimension: .absolute(34)), 211 | elementKind: UICollectionView.elementKindSectionHeader, 212 | alignment: .top) 213 | header.contentInsets.leading = nestedGroup.contentInsets.leading 214 | header.contentInsets.trailing = nestedGroup.contentInsets.trailing 215 | 216 | let section = NSCollectionLayoutSection(group: nestedGroup) 217 | section.boundarySupplementaryItems = [header] 218 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) 219 | section.orthogonalScrollingBehavior = .groupPaging 220 | section.visibleItemsInvalidationHandler = { _, _, _ in } // If this isn't defined, there is a bug in UICVCompositional Layout that will fail to update sizes of cells 221 | 222 | return section 223 | } 224 | } 225 | } 226 | } 227 | } 228 | 229 | struct AppStoreScreen_Previews: PreviewProvider 230 | { 231 | static var previews: some View 232 | { 233 | AppStoreScreen() 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/AppStore/AppViews.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | struct GetButton: View 6 | { 7 | var body: some View 8 | { 9 | Button(action: { 10 | // Do something 11 | }) 12 | { 13 | Text("GET") 14 | .fontWeight(.bold) 15 | .padding(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) 16 | } 17 | .background(Capsule().fill(Color(.systemGray6))) 18 | } 19 | } 20 | 21 | struct AppViewFeature: View 22 | { 23 | var app: App 24 | var body: some View 25 | { 26 | VStack(alignment: .leading) 27 | { 28 | Text(app.appName) 29 | .font(.headline) 30 | .lineLimit(1) 31 | Text(app.caption) 32 | .font(.body) 33 | .foregroundColor(.secondary) 34 | .lineLimit(2) 35 | ASRemoteImageView(app.featureImageURL) 36 | .cornerRadius(16) 37 | .clipped() 38 | } 39 | } 40 | } 41 | 42 | struct AppViewLarge: View 43 | { 44 | var app: App 45 | var body: some View 46 | { 47 | HStack(alignment: .top) 48 | { 49 | ASRemoteImageView(app.url) 50 | .aspectRatio(1, contentMode: .fit) 51 | .cornerRadius(16) 52 | .clipped() 53 | VStack(alignment: .leading) 54 | { 55 | Text(app.appName) 56 | .font(.headline) 57 | Text(app.caption) 58 | .font(.caption) 59 | .foregroundColor(.secondary) 60 | Spacer() 61 | GetButton() 62 | } 63 | Spacer() 64 | } 65 | } 66 | } 67 | 68 | struct AppViewCompact: View 69 | { 70 | var app: App 71 | var body: some View 72 | { 73 | HStack(alignment: .center) 74 | { 75 | ASRemoteImageView(app.url) 76 | .aspectRatio(1, contentMode: .fit) 77 | .cornerRadius(16) 78 | .clipped() 79 | VStack(alignment: .leading) 80 | { 81 | Text(app.appName) 82 | .font(.headline) 83 | .lineLimit(1) 84 | Text(app.caption) 85 | .font(.caption) 86 | .foregroundColor(.secondary) 87 | .lineLimit(2) 88 | } 89 | Spacer() 90 | GetButton() 91 | } 92 | } 93 | } 94 | 95 | struct NumberView_Previews: PreviewProvider 96 | { 97 | static var previews: some View 98 | { 99 | Group 100 | { 101 | AppViewFeature(app: App.randomApp(1)) 102 | .previewLayout(.fixed(width: 400, height: 300)) 103 | 104 | AppViewLarge(app: App.randomApp(1)) 105 | .previewLayout(.fixed(width: 400, height: 125)) 106 | 107 | AppViewCompact(app: App.randomApp(1)) 108 | .previewLayout(.fixed(width: 400, height: 83)) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/InstaFeed/InstaFeedScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | import UIKit 6 | 7 | struct InstaFeedScreen: View 8 | { 9 | @State var storiesData: [Post] = DataSource.postsForInstaSection(0, number: 12) 10 | @State var data: [[Post]] = (0 ... 1).map { DataSource.postsForInstaSection($0 + 1) } 11 | 12 | var storiesCollectionView: some View 13 | { 14 | ASCollectionView( 15 | section: 16 | ASCollectionViewSection( 17 | id: 0, 18 | data: storiesData, 19 | onCellEvent: onCellEventStories) 20 | { item, _ in 21 | StoryView(post: item) 22 | }) 23 | .layout(scrollDirection: .horizontal) 24 | { 25 | .list(itemSize: .absolute(100), sectionInsets: NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) 26 | } 27 | .onReachedBoundary 28 | { boundary in 29 | print("Reached the \(boundary) boundary") 30 | } 31 | .scrollIndicatorsEnabled(horizontal: false, vertical: false) 32 | .frame(height: 100) 33 | } 34 | 35 | var storiesSection: ASTableViewSection 36 | { 37 | ASTableViewSection(id: 0) 38 | { 39 | storiesCollectionView 40 | } 41 | .cacheCells() // Used so that the nested collectionView is cached even when offscreen (which maintains scroll position etc) 42 | } 43 | 44 | var postSections: [ASTableViewSection] 45 | { 46 | data.enumerated().map 47 | { i, sectionData in 48 | ASTableViewSection( 49 | id: i + 1, 50 | data: sectionData, 51 | onCellEvent: onCellEventPosts) 52 | { item, _ in 53 | PostView(post: item) 54 | } 55 | .tableViewSetEstimatedSizes(headerHeight: 50) // Optional: Provide reasonable estimated heights for this section 56 | .sectionHeader 57 | { 58 | VStack(spacing: 0) 59 | { 60 | Text("Section \(i)") 61 | .padding(EdgeInsets(top: 4, leading: 20, bottom: 4, trailing: 20)) 62 | .frame(maxWidth: .infinity, alignment: .leading) 63 | Divider() 64 | } 65 | .background(Color(.secondarySystemBackground)) 66 | } 67 | } 68 | } 69 | 70 | var body: some View 71 | { 72 | ASTableView 73 | { 74 | storiesSection // An ASSection 75 | postSections // An array of ASSection's 76 | } 77 | .onReachedBottom 78 | { 79 | self.loadMoreContent() // REACHED BOTTOM, LOADING MORE CONTENT 80 | } 81 | .separatorsEnabled(false) 82 | .onPullToRefresh 83 | { endRefreshing in 84 | print("PULL TO REFRESH") 85 | Timer.scheduledTimer(withTimeInterval: 2, repeats: false) 86 | { _ in 87 | endRefreshing() 88 | } 89 | } 90 | .navigationBarTitle("Insta Feed (tableview)", displayMode: .inline) 91 | } 92 | 93 | func loadMoreContent() 94 | { 95 | let a = data.count 96 | data.append(DataSource.postsForInstaSection(a + 1)) 97 | } 98 | 99 | func onCellEventStories(_ event: CellEvent) 100 | { 101 | switch event 102 | { 103 | case let .onAppear(item): 104 | ASRemoteImageManager.shared.load(item.url) 105 | case let .onDisappear(item): 106 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 107 | case let .prefetchForData(data): 108 | for item in data 109 | { 110 | ASRemoteImageManager.shared.load(item.url) 111 | } 112 | case let .cancelPrefetchForData(data): 113 | for item in data 114 | { 115 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 116 | } 117 | } 118 | } 119 | 120 | func onCellEventPosts(_ event: CellEvent) 121 | { 122 | switch event 123 | { 124 | case let .onAppear(item): 125 | ASRemoteImageManager.shared.load(item.url) 126 | ASRemoteImageManager.shared.load(item.usernamePhotoURL) 127 | case let .onDisappear(item): 128 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 129 | ASRemoteImageManager.shared.cancelLoad(for: item.usernamePhotoURL) 130 | case let .prefetchForData(data): 131 | for item in data 132 | { 133 | ASRemoteImageManager.shared.load(item.url) 134 | ASRemoteImageManager.shared.load(item.usernamePhotoURL) 135 | } 136 | case let .cancelPrefetchForData(data): 137 | for item in data 138 | { 139 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 140 | ASRemoteImageManager.shared.cancelLoad(for: item.usernamePhotoURL) 141 | } 142 | } 143 | } 144 | } 145 | 146 | struct FeedView_Previews: PreviewProvider 147 | { 148 | static var previews: some View 149 | { 150 | InstaFeedScreen() 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/InstaFeed/PostView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct PostView: View 7 | { 8 | @State var liked: Bool = false 9 | @State var bookmarked: Bool = false 10 | 11 | @State var captionExpanded: Bool = false 12 | 13 | @Environment(\.invalidateCellLayout) var invalidateCellLayout 14 | 15 | var post: Post 16 | 17 | var header: some View 18 | { 19 | HStack 20 | { 21 | ASRemoteImageView(post.usernamePhotoURL) 22 | .aspectRatio(contentMode: .fill) 23 | .frame(width: 40, height: 40) 24 | .clipShape(Circle()) 25 | .padding(.leading) 26 | VStack(alignment: .leading) 27 | { 28 | Text(post.username).fontWeight(.bold) 29 | Text(post.location) 30 | } 31 | Spacer() 32 | Image(systemName: "ellipsis") 33 | .padding() 34 | } 35 | .fixedSize(horizontal: false, vertical: true) 36 | } 37 | 38 | var buttonBar: some View 39 | { 40 | HStack 41 | { 42 | Image(systemName: self.liked ? "heart.fill" : "heart") 43 | .renderingMode(.template) 44 | .foregroundColor(self.liked ? .red : Color(.label)) 45 | .onTapGesture 46 | { 47 | self.liked.toggle() 48 | } 49 | Image(systemName: "bubble.right") 50 | Image(systemName: "paperplane") 51 | Spacer() 52 | Image(systemName: self.bookmarked ? "bookmark.fill" : "bookmark") 53 | .renderingMode(.template) 54 | .foregroundColor(self.bookmarked ? .yellow : Color(.label)) 55 | .onTapGesture 56 | { 57 | self.bookmarked.toggle() 58 | } 59 | } 60 | .font(.system(size: 28)) 61 | .padding() 62 | .fixedSize(horizontal: false, vertical: true) 63 | } 64 | 65 | var textContent: some View 66 | { 67 | VStack(alignment: .leading, spacing: 10) 68 | { 69 | Text("Liked by ") + Text("apptekstudios").fontWeight(.bold) + Text(" and ") + Text("others").fontWeight(.bold) 70 | Group 71 | { 72 | Text("\(post.username) ").fontWeight(.bold) + Text(post.caption) 73 | } 74 | .lineLimit(self.captionExpanded ? nil : 2) 75 | .truncationMode(.tail) 76 | .onTapGesture 77 | { 78 | self.captionExpanded.toggle() 79 | self.invalidateCellLayout?(false) 80 | } 81 | Text("View all \(post.comments) comments").foregroundColor(Color(.systemGray)) 82 | } 83 | .padding([.leading, .trailing]) 84 | .frame(maxWidth: .infinity, alignment: .leading) 85 | .fixedSize(horizontal: false, vertical: true) 86 | } 87 | 88 | var body: some View 89 | { 90 | VStack 91 | { 92 | header 93 | ASRemoteImageView(post.url) 94 | .aspectRatio(post.aspectRatio, contentMode: .fill) 95 | .gesture( 96 | TapGesture(count: 2).onEnded 97 | { 98 | self.liked.toggle() 99 | } 100 | ) 101 | buttonBar 102 | textContent 103 | Spacer().layoutPriority(2) 104 | } 105 | .padding([.top, .bottom]) 106 | } 107 | } 108 | 109 | struct PostView_Previews: PreviewProvider 110 | { 111 | static var previews: some View 112 | { 113 | PostView(post: Post.randomPost(Int.random(in: 0 ... 1000), aspectRatio: 1)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/InstaFeed/StoryView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | struct StoryView: View 6 | { 7 | var post: Post 8 | 9 | var body: some View 10 | { 11 | VStack 12 | { 13 | ASRemoteImageView(post.url) 14 | .aspectRatio(contentMode: .fill) 15 | .clipShape(Circle()) 16 | .frame(width: 50, height: 50) 17 | .fixedSize() 18 | Text(post.username) 19 | .lineLimit(1) 20 | .font(.caption) 21 | .truncationMode(.tail) 22 | } 23 | } 24 | } 25 | 26 | struct StoryView_Previews: PreviewProvider 27 | { 28 | static var previews: some View 29 | { 30 | StoryView(post: Post.randomPost(Int.random(in: 0 ... 1000), aspectRatio: 1)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/MagazineLayout/CustomDelegate.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import MagazineLayout 5 | import UIKit 6 | 7 | class ASCollectionViewMagazineLayoutDelegate: ASCollectionViewDelegate, UICollectionViewDelegateMagazineLayout 8 | { 9 | override func collectionViewSelfSizingSettings(forContext: ASSelfSizingContext) -> ASSelfSizingConfig? 10 | { 11 | ASSelfSizingConfig(selfSizeHorizontally: false, selfSizeVertically: true) 12 | } 13 | 14 | override var collectionViewContentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior 15 | { 16 | .always 17 | } 18 | 19 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeModeForItemAt indexPath: IndexPath) -> MagazineLayoutItemSizeMode 20 | { 21 | let rowIsThree = (indexPath.item % 5) < 3 22 | let widthMode = rowIsThree ? MagazineLayoutItemWidthMode.thirdWidth : MagazineLayoutItemWidthMode.halfWidth 23 | let heightMode = MagazineLayoutItemHeightMode.dynamic 24 | return MagazineLayoutItemSizeMode(widthMode: widthMode, heightMode: heightMode) 25 | } 26 | 27 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForHeaderInSectionAtIndex index: Int) -> MagazineLayoutHeaderVisibilityMode 28 | { 29 | .visible(heightMode: .dynamic, pinToVisibleBounds: true) 30 | } 31 | 32 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForFooterInSectionAtIndex index: Int) -> MagazineLayoutFooterVisibilityMode 33 | { 34 | .visible(heightMode: .dynamic, pinToVisibleBounds: false) 35 | } 36 | 37 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, visibilityModeForBackgroundInSectionAtIndex index: Int) -> MagazineLayoutBackgroundVisibilityMode 38 | { 39 | .hidden 40 | } 41 | 42 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, horizontalSpacingForItemsInSectionAtIndex index: Int) -> CGFloat 43 | { 44 | 12 45 | } 46 | 47 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, verticalSpacingForElementsInSectionAtIndex index: Int) -> CGFloat 48 | { 49 | 12 50 | } 51 | 52 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForSectionAtIndex index: Int) -> UIEdgeInsets 53 | { 54 | UIEdgeInsets(top: 0, left: 8, bottom: 24, right: 8) 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetsForItemsInSectionAtIndex index: Int) -> UIEdgeInsets 58 | { 59 | UIEdgeInsets(top: 24, left: 0, bottom: 24, right: 0) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/MagazineLayout/MagazineLayoutScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import MagazineLayout 5 | import SwiftUI 6 | import UIKit 7 | 8 | struct MagazineLayoutScreen: View 9 | { 10 | @State var data: [[Post]] = (0 ... 5).map 11 | { 12 | DataSource.postsForGridSection($0, number: 10) 13 | } 14 | 15 | var sections: [ASCollectionViewSection] 16 | { 17 | data.enumerated().map 18 | { (offset, sectionData) -> ASCollectionViewSection in 19 | ASCollectionViewSection( 20 | id: offset, 21 | data: sectionData, 22 | onCellEvent: onCellEvent, 23 | contextMenuProvider: contextMenuProvider) 24 | { item, _ in 25 | ASRemoteImageView(item.url) 26 | .aspectRatio(1, contentMode: .fit) 27 | } 28 | .sectionSupplementary(ofKind: MagazineLayout.SupplementaryViewKind.sectionHeader) 29 | { 30 | Text("Section \(offset)") 31 | .padding() 32 | .frame(maxWidth: .infinity, alignment: .leading) 33 | .background(Color.blue) 34 | } 35 | } 36 | } 37 | 38 | var body: some View 39 | { 40 | ASCollectionView(sections: self.sections) 41 | .layout { MagazineLayout() } 42 | .onReachedBoundary 43 | { boundary in 44 | print("Reached the \(boundary) boundary") 45 | } 46 | .customDelegate(ASCollectionViewMagazineLayoutDelegate.init) 47 | .edgesIgnoringSafeArea(.all) 48 | .navigationBarTitle("Magazine Layout (custom delegate)", displayMode: .inline) 49 | } 50 | 51 | func onCellEvent(_ event: CellEvent) 52 | { 53 | switch event 54 | { 55 | case let .onAppear(item): 56 | ASRemoteImageManager.shared.load(item.url) 57 | case let .onDisappear(item): 58 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 59 | case let .prefetchForData(data): 60 | for item in data 61 | { 62 | ASRemoteImageManager.shared.load(item.url) 63 | } 64 | case let .cancelPrefetchForData(data): 65 | for item in data 66 | { 67 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 68 | } 69 | } 70 | } 71 | 72 | func contextMenuProvider(index: Int, post: Post) -> UIContextMenuConfiguration? 73 | { 74 | let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) 75 | { (_) -> UIMenu? in 76 | let testAction = UIAction(title: "Test") 77 | { _ in 78 | // 79 | } 80 | return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [testAction]) 81 | } 82 | return configuration 83 | } 84 | } 85 | 86 | struct MagazineLayoutScreen_Previews: PreviewProvider 87 | { 88 | static var previews: some View 89 | { 90 | MagazineLayoutScreen() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/PhotoGrid/PhotoGridScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | import UIKit 6 | 7 | struct PhotoGridScreen: View 8 | { 9 | @State var data: [Post] = DataSource.postsForGridSection(1, number: 1000) 10 | @State var selectedIndexes: Set = [] 11 | 12 | @Environment(\.editMode) private var editMode 13 | var isEditing: Bool 14 | { 15 | editMode?.wrappedValue.isEditing ?? false 16 | } 17 | 18 | typealias SectionID = Int 19 | 20 | var section: ASCollectionViewSection 21 | { 22 | ASCollectionViewSection( 23 | id: 0, 24 | data: data, 25 | selectionMode: isEditing ? .selectMultiple($selectedIndexes) : .none, 26 | onCellEvent: onCellEvent) 27 | { item, state in 28 | ZStack(alignment: .bottomTrailing) 29 | { 30 | GeometryReader 31 | { geom in 32 | NavigationLink(destination: self.destinationForItem(item)) 33 | { 34 | ASRemoteImageView(item.url) 35 | .aspectRatio(1, contentMode: .fill) 36 | .frame(width: geom.size.width, height: geom.size.height) 37 | .clipped() 38 | .opacity(self.isEditing ? (state.isSelected ? 1 : 0.7) : 1) 39 | } 40 | .buttonStyle(NeutralButtonStyle()) 41 | .disabled(self.isEditing) 42 | } 43 | 44 | self.selectionIndicator(isSelected: state.isSelected, isHighlighted: state.isHighlighted) 45 | } 46 | } 47 | } 48 | 49 | var body: some View 50 | { 51 | ASCollectionView( 52 | editMode: isEditing, 53 | section: section) 54 | .layout(self.layout) 55 | .edgesIgnoringSafeArea(.all) 56 | .navigationBarTitle("Explore", displayMode: .large) 57 | .navigationBarItems( 58 | trailing: 59 | HStack(spacing: 20) 60 | { 61 | if self.isEditing 62 | { 63 | Button(action: { 64 | withAnimation 65 | { 66 | // We want the cell removal to be animated, so explicitly specify `withAnimation` 67 | self.data.remove(atOffsets: IndexSet(self.selectedIndexes)) 68 | } 69 | }) 70 | { 71 | Image(systemName: "trash") 72 | } 73 | } 74 | 75 | EditButton() 76 | }) 77 | } 78 | 79 | private func selectionIndicator(isSelected: Bool, isHighlighted: Bool) -> some View 80 | { 81 | let scale: CGFloat 82 | switch (isSelected, isHighlighted) 83 | { 84 | case (true, true): scale = 0.75 85 | case (true, false): scale = 1 86 | case (false, true): scale = 1.15 87 | case (false, false): scale = 0 88 | } 89 | 90 | return ZStack 91 | { 92 | Circle() 93 | .fill(Color.blue) 94 | Circle() 95 | .strokeBorder(Color.white, lineWidth: 2) 96 | Image(systemName: "checkmark") 97 | .font(.system(size: 10, weight: .bold)) 98 | .foregroundColor(.white) 99 | } 100 | .frame(width: 20, height: 20) 101 | .padding(10) 102 | .scaleEffect(scale) 103 | .animation(Animation.easeInOut(duration: 0.15)) 104 | } 105 | 106 | func onCellEvent(_ event: CellEvent) 107 | { 108 | switch event 109 | { 110 | case let .onAppear(item): 111 | ASRemoteImageManager.shared.load(item.url) 112 | case let .onDisappear(item): 113 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 114 | case let .prefetchForData(data): 115 | for item in data 116 | { 117 | ASRemoteImageManager.shared.load(item.url) 118 | } 119 | case let .cancelPrefetchForData(data): 120 | for item in data 121 | { 122 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 123 | } 124 | } 125 | } 126 | 127 | func contextMenuProvider(int: Int, post: Post) -> UIContextMenuConfiguration? 128 | { 129 | let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) 130 | { (_) -> UIMenu? in 131 | let testAction = UIAction(title: "Do nothing") 132 | { _ in 133 | // 134 | } 135 | let testAction2 = UIAction(title: "Try dragging the photo") 136 | { _ in 137 | // 138 | } 139 | return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [testAction, testAction2]) 140 | } 141 | return configuration 142 | } 143 | 144 | func destinationForItem(_ item: Post) -> some View 145 | { 146 | ScrollView 147 | { 148 | PostView(post: item) 149 | .onAppear 150 | { 151 | ASRemoteImageManager.shared.load(item.url) 152 | ASRemoteImageManager.shared.load(item.usernamePhotoURL) 153 | } 154 | } 155 | .navigationBarTitle("", displayMode: .inline) 156 | } 157 | } 158 | 159 | extension PhotoGridScreen 160 | { 161 | var layout: ASCollectionLayout 162 | { 163 | ASCollectionLayout(scrollDirection: .vertical, interSectionSpacing: 0) 164 | { 165 | ASCollectionLayoutSection 166 | { environment in 167 | let isWide = environment.container.effectiveContentSize.width > 500 168 | let gridBlockSize = environment.container.effectiveContentSize.width / (isWide ? 5 : 3) 169 | let gridItemInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) 170 | let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize), heightDimension: .absolute(gridBlockSize)) 171 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 172 | item.contentInsets = gridItemInsets 173 | let verticalGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize), heightDimension: .absolute(gridBlockSize * 2)) 174 | let verticalGroup = NSCollectionLayoutGroup.vertical(layoutSize: verticalGroupSize, subitem: item, count: 2) 175 | 176 | let featureItemSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize * 2), heightDimension: .absolute(gridBlockSize * 2)) 177 | let featureItem = NSCollectionLayoutItem(layoutSize: featureItemSize) 178 | featureItem.contentInsets = gridItemInsets 179 | 180 | let fullWidthItemSize = NSCollectionLayoutSize(widthDimension: .absolute(environment.container.effectiveContentSize.width), heightDimension: .absolute(gridBlockSize * 2)) 181 | let fullWidthItem = NSCollectionLayoutItem(layoutSize: fullWidthItemSize) 182 | fullWidthItem.contentInsets = gridItemInsets 183 | 184 | let verticalAndFeatureGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize * 2)) 185 | let verticalAndFeatureGroupA = NSCollectionLayoutGroup.horizontal(layoutSize: verticalAndFeatureGroupSize, subitems: isWide ? [verticalGroup, verticalGroup, featureItem, verticalGroup] : [verticalGroup, featureItem]) 186 | let verticalAndFeatureGroupB = NSCollectionLayoutGroup.horizontal(layoutSize: verticalAndFeatureGroupSize, subitems: isWide ? [verticalGroup, featureItem, verticalGroup, verticalGroup] : [featureItem, verticalGroup]) 187 | 188 | let rowGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize)) 189 | let rowGroup = NSCollectionLayoutGroup.horizontal(layoutSize: rowGroupSize, subitem: item, count: isWide ? 5 : 3) 190 | 191 | let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize * 8)) 192 | let outerGroup = NSCollectionLayoutGroup.vertical(layoutSize: outerGroupSize, subitems: [verticalAndFeatureGroupA, rowGroup, fullWidthItem, verticalAndFeatureGroupB, rowGroup]) 193 | 194 | let section = NSCollectionLayoutSection(group: outerGroup) 195 | return section 196 | } 197 | } 198 | } 199 | 200 | var dragDropConfig: ASDragDropConfig 201 | { 202 | ASDragDropConfig(dataBinding: $data) 203 | .canDragItem 204 | { (indexPath) -> Bool in 205 | true 206 | // indexPath.item != 0 // eg. prevent dragging/moving the first item 207 | } 208 | .canMoveItem 209 | { (from, to) -> Bool in 210 | // You could add a check here to prevent moving between certain sections etc. 211 | true 212 | } 213 | .dragItemProvider 214 | { item in 215 | NSItemProvider(object: item.url as NSURL) 216 | } 217 | } 218 | } 219 | 220 | struct GridView_Previews: PreviewProvider 221 | { 222 | static var previews: some View 223 | { 224 | PhotoGridScreen() 225 | } 226 | } 227 | 228 | struct NeutralButtonStyle: ButtonStyle 229 | { 230 | func makeBody(configuration: Configuration) -> some View 231 | { 232 | configuration.label 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Reminders/GroupLarge.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | struct GroupLarge: View 6 | { 7 | var model: GroupModel 8 | var background: Background 9 | 10 | var body: some View 11 | { 12 | VStack(alignment: .leading) 13 | { 14 | HStack(alignment: .center) 15 | { 16 | Image(systemName: model.icon) 17 | .font(.system(size: 16, weight: .regular)) 18 | .padding(14) 19 | .foregroundColor(.white) 20 | .background( 21 | Circle().fill(model.color) 22 | ) 23 | Spacer() 24 | model.contentCount.map 25 | { 26 | Text("\($0)") 27 | .font(.title) 28 | .bold() 29 | } 30 | } 31 | Text(model.title) 32 | .bold() 33 | .multilineTextAlignment(.leading) 34 | .foregroundColor(Color(.secondaryLabel)) 35 | } 36 | .padding() 37 | .background(background) 38 | .clipShape(RoundedRectangle(cornerRadius: 12)) 39 | } 40 | } 41 | 42 | struct GroupLarge_Previews: PreviewProvider 43 | { 44 | static var previews: some View 45 | { 46 | ZStack 47 | { 48 | Color(.secondarySystemBackground) 49 | GroupLarge(model: .demo, background: Color(.secondarySystemGroupedBackground)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Reminders/GroupModel.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct GroupModel: Identifiable 7 | { 8 | var icon: String 9 | var title: String 10 | var contentCount: Int? = Int.random(in: 0 ... 20) 11 | var color: Color = [Color.red, Color.orange, Color.blue, Color.purple].randomElement()! 12 | 13 | static var demo = GroupModel(icon: "paperplane", title: "Test category", contentCount: 19) 14 | 15 | var id: String { title } 16 | } 17 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Reminders/GroupSmall.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | struct GroupSmall: View 6 | { 7 | var model: GroupModel 8 | 9 | var body: some View 10 | { 11 | HStack(alignment: .center) 12 | { 13 | Image(systemName: model.icon) 14 | .font(.system(size: 16, weight: .regular)) 15 | .padding(14) 16 | .foregroundColor(.white) 17 | .background( 18 | Circle().fill(model.color) 19 | ) 20 | 21 | Text(model.title) 22 | .multilineTextAlignment(.leading) 23 | .foregroundColor(Color(.label)) 24 | 25 | Spacer() 26 | model.contentCount.map 27 | { 28 | Text("\($0)") 29 | } 30 | } 31 | .padding(10) 32 | } 33 | } 34 | 35 | struct GroupSmall_Previews: PreviewProvider 36 | { 37 | static var previews: some View 38 | { 39 | GroupSmall(model: .demo) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Reminders/RemindersScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct RemindersScreen: View 7 | { 8 | enum Section 9 | { 10 | case upper 11 | case list 12 | case addNew 13 | case footnote 14 | } 15 | 16 | var upperData: [GroupModel] = [GroupModel(icon: "calendar", title: "Today", color: .blue), 17 | GroupModel(icon: "clock.fill", title: "Scheduled", color: .orange), 18 | GroupModel(icon: "tray.fill", title: "All", color: .gray), 19 | GroupModel(icon: "flag.fill", title: "Flagged", color: .red)] 20 | var lowerData: [GroupModel] = [GroupModel(icon: "list.bullet", title: "Todo"), 21 | GroupModel(icon: "cart.fill", title: "Groceries"), 22 | GroupModel(icon: "house.fill", title: "House renovation"), 23 | GroupModel(icon: "book.fill", title: "Reading list")] 24 | 25 | let addNewModel = GroupModel(icon: "plus", title: "Create new list", contentCount: nil, color: .green) 26 | 27 | var body: some View 28 | { 29 | ASCollectionView 30 | { 31 | ASCollectionViewSection
(id: .upper, data: self.upperData) 32 | { model, cellContext in 33 | GroupLarge(model: model, background: Color(.secondarySystemGroupedBackground).brightness(cellContext.isHighlighted ? -0.2 : 0)) 34 | } 35 | 36 | ASCollectionViewSection
(id: .list, data: self.lowerData) 37 | { model, info in 38 | VStack(spacing: 0) 39 | { 40 | GroupSmall(model: model) 41 | if !info.isLastInSection 42 | { 43 | Divider() 44 | } 45 | } 46 | } 47 | .sectionHeader 48 | { 49 | Text("My Lists") 50 | .font(.headline) 51 | .bold() 52 | .padding() 53 | .frame(maxWidth: .infinity, alignment: .leading) 54 | } 55 | 56 | ASCollectionViewSection
(id: .addNew) 57 | { 58 | GroupSmall(model: self.addNewModel) 59 | } 60 | 61 | ASCollectionViewSection
(id: .footnote) 62 | { 63 | Text("Try rotating the screen") 64 | .padding() 65 | .frame(maxWidth: .infinity, alignment: .center) 66 | } 67 | } 68 | .layout(self.layout) 69 | .contentInsets(.init(top: 20, left: 0, bottom: 20, right: 0)) 70 | .alwaysBounceVertical() 71 | .background(Color(.systemGroupedBackground)) 72 | .edgesIgnoringSafeArea(.all) 73 | .navigationBarTitle("Reminders", displayMode: .inline) 74 | } 75 | 76 | let groupBackgroundElementID = UUID().uuidString 77 | 78 | var layout: ASCollectionLayout
79 | { 80 | ASCollectionLayout
(interSectionSpacing: 20) 81 | { sectionID in 82 | switch sectionID 83 | { 84 | case .upper: 85 | return .grid( 86 | layoutMode: .adaptive(withMinItemSize: 165), 87 | itemSpacing: 20, 88 | lineSpacing: 20, 89 | itemSize: .estimated(90)) 90 | case .list, .addNew, .footnote: 91 | return ASCollectionLayoutSection 92 | { 93 | let itemSize = NSCollectionLayoutSize( 94 | widthDimension: .fractionalWidth(1.0), 95 | heightDimension: .estimated(60)) 96 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 97 | 98 | let groupSize = NSCollectionLayoutSize( 99 | widthDimension: .fractionalWidth(1.0), 100 | heightDimension: .estimated(60)) 101 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 102 | 103 | let section = NSCollectionLayoutSection(group: group) 104 | section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20) 105 | 106 | let supplementarySize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) 107 | let headerSupplementary = NSCollectionLayoutBoundarySupplementaryItem( 108 | layoutSize: supplementarySize, 109 | elementKind: UICollectionView.elementKindSectionHeader, 110 | alignment: .top) 111 | let footerSupplementary = NSCollectionLayoutBoundarySupplementaryItem( 112 | layoutSize: supplementarySize, 113 | elementKind: UICollectionView.elementKindSectionFooter, 114 | alignment: .bottom) 115 | section.boundarySupplementaryItems = [headerSupplementary, footerSupplementary] 116 | 117 | let sectionBackgroundDecoration = NSCollectionLayoutDecorationItem.background(elementKind: self.groupBackgroundElementID) 118 | sectionBackgroundDecoration.contentInsets = section.contentInsets 119 | section.decorationItems = [sectionBackgroundDecoration] 120 | 121 | return section 122 | } 123 | } 124 | } 125 | .decorationView(GroupBackground.self, forDecorationViewOfKind: groupBackgroundElementID) 126 | } 127 | } 128 | 129 | struct GroupBackground: View, Decoration 130 | { 131 | let cornerRadius: CGFloat = 12 132 | var body: some View 133 | { 134 | RoundedRectangle(cornerRadius: cornerRadius) 135 | .fill(Color(.secondarySystemGroupedBackground)) 136 | } 137 | } 138 | 139 | struct RemindersScreen_Previews: PreviewProvider 140 | { 141 | static var previews: some View 142 | { 143 | RemindersScreen() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/TableViewDragAndDrop/TableViewDragAndDropScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct TableViewDragAndDropScreen: View 7 | { 8 | @State var groupA: [String] = (0 ... 4).map { "Item A-\($0)" } 9 | @State var groupB: [String] = (0 ... 4).map { "Item B-\($0)" } 10 | @State var groupC: [String] = (0 ... 4).map { "Item C-\($0)" } 11 | @State var groupD: [String] = (0 ... 4).map { "Item D-\($0)" } 12 | 13 | var body: some View 14 | { 15 | VStack 16 | { 17 | Text("Drag within a tableview to move.\nDrag between tableviews to copy.") 18 | .padding() 19 | HStack 20 | { 21 | ASTableView 22 | { 23 | ASSection( 24 | id: 0, 25 | data: groupA, 26 | dataID: \.self, 27 | dragDropConfig: ASDragDropConfig(dataBinding: $groupA), 28 | onSwipeToDelete: { index, _ -> Bool in 29 | withAnimation 30 | { 31 | _ = self.groupA.remove(at: index) 32 | } 33 | return true 34 | }) 35 | { item, _ in 36 | Text(item) 37 | .padding() 38 | .frame(maxWidth: .infinity, alignment: .leading) 39 | } 40 | .sectionHeader 41 | { 42 | header("Section A") 43 | } 44 | ASSection( 45 | id: 1, 46 | data: groupB, 47 | dataID: \.self, 48 | dragDropConfig: ASDragDropConfig(dataBinding: $groupB), 49 | onSwipeToDelete: { index, _ -> Bool in 50 | withAnimation 51 | { 52 | _ = self.groupB.remove(at: index) 53 | } 54 | return true 55 | }) 56 | { item, _ in 57 | Text(item) 58 | .padding() 59 | .frame(maxWidth: .infinity, alignment: .leading) 60 | } 61 | .sectionHeader 62 | { 63 | header("Section B") 64 | } 65 | } 66 | Color.blue.frame(width: 10) 67 | ASTableView 68 | { 69 | ASSection( 70 | id: 0, 71 | data: groupC, 72 | dataID: \.self, 73 | dragDropConfig: ASDragDropConfig(dataBinding: $groupC), 74 | onSwipeToDelete: { index, _ -> Bool in 75 | withAnimation 76 | { 77 | _ = self.groupC.remove(at: index) 78 | } 79 | return true 80 | }) 81 | { item, _ in 82 | Text(item) 83 | .padding() 84 | .frame(maxWidth: .infinity, alignment: .leading) 85 | } 86 | .sectionHeader 87 | { 88 | header("Section C") 89 | } 90 | ASSection( 91 | id: 1, 92 | data: groupD, 93 | dataID: \.self, 94 | dragDropConfig: ASDragDropConfig(dataBinding: $groupD), 95 | onSwipeToDelete: { index, _ -> Bool in 96 | withAnimation 97 | { 98 | _ = self.groupD.remove(at: index) 99 | } 100 | return true 101 | }) 102 | { item, _ in 103 | Text(item) 104 | .padding() 105 | .frame(maxWidth: .infinity, alignment: .leading) 106 | } 107 | .sectionHeader 108 | { 109 | header("Section D") 110 | } 111 | } 112 | } 113 | } 114 | .navigationBarTitle("Drag & drop", displayMode: .inline) 115 | } 116 | 117 | func header(_ string: String) -> some View 118 | { 119 | Text(string) 120 | .padding() 121 | .frame(maxWidth: .infinity, alignment: .leading) 122 | .background(Color(.secondarySystemBackground)) 123 | } 124 | } 125 | 126 | struct TableViewDragAndDropScreen_Previews: PreviewProvider 127 | { 128 | static var previews: some View 129 | { 130 | TableViewDragAndDropScreen() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Tags/TagStore.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Combine 4 | import Foundation 5 | 6 | struct Item: Identifiable 7 | { 8 | var id: Int 9 | var displayString: String 10 | } 11 | 12 | class TagStore: ObservableObject 13 | { 14 | init() 15 | {} 16 | 17 | @Published var items: [Item] = TagStore.randomItems() 18 | 19 | func refreshStore() 20 | { 21 | items = TagStore.randomItems() 22 | } 23 | 24 | fileprivate static let allWords = ["thisisaveryverylongtagthatshouldrequiremorethanonelinetofit", "alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "maiores", "doloribus", "asperiores", "repellat"] 25 | 26 | static func randomItems() -> [Item] 27 | { 28 | TagStore.allWords.indices.shuffled()[0 ... Int.random(in: 14 ... 30)].map 29 | { 30 | Item(id: $0, displayString: TagStore.allWords[$0]) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Tags/TagsScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct TagsScreen: View 7 | { 8 | @ObservedObject var store = TagStore() 9 | 10 | var body: some View 11 | { 12 | ScrollView(.vertical) 13 | { 14 | VStack(alignment: .leading, spacing: 20) 15 | { 16 | Text("This screen has an ASCollectionView embedded into a SwiftUI scrollview") 17 | .multilineTextAlignment(.center) 18 | .fixedSize(horizontal: false, vertical: true) 19 | .frame(maxWidth: .infinity) 20 | .padding() 21 | HStack 22 | { 23 | Spacer() 24 | Text("Tap this button to reload new tags") 25 | .padding() 26 | .background(Color(.secondarySystemBackground)) 27 | Spacer() 28 | } 29 | .onTapGesture 30 | { 31 | self.store.refreshStore() 32 | } 33 | Text("Tags:") 34 | .font(.title) 35 | 36 | ASCollectionView( 37 | section: 38 | ASCollectionViewSection(id: 0, data: store.items) 39 | { item, _ in 40 | Text(item.displayString) 41 | .fixedSize(horizontal: false, vertical: true) 42 | .padding(5) 43 | .background(Color(.systemGray)) 44 | .cornerRadius(5) 45 | }.selfSizingConfig 46 | { _ in 47 | ASSelfSizingConfig(canExceedCollectionWidth: false) 48 | } 49 | ) 50 | .layout 51 | { 52 | let fl = AlignedFlowLayout() 53 | fl.estimatedItemSize = UICollectionViewFlowLayout.automaticSize 54 | return fl 55 | } 56 | .fitContentSize(dimension: .vertical) 57 | Text("This is another view in the VStack, it shows how the collectionView above fits itself to the content.") 58 | .padding() 59 | .frame(maxWidth: .infinity) 60 | .foregroundColor(Color(.secondaryLabel)) 61 | .fixedSize(horizontal: false, vertical: true) 62 | .background(Color(.secondarySystemBackground)) 63 | } 64 | .padding() 65 | } 66 | .navigationBarTitle("Tags", displayMode: .inline) 67 | } 68 | } 69 | 70 | class AlignedFlowLayout: UICollectionViewFlowLayout 71 | { 72 | override init() 73 | { 74 | super.init() 75 | } 76 | 77 | @available(*, unavailable) 78 | required init?(coder: NSCoder) 79 | { 80 | fatalError("init(coder:) has not been implemented") 81 | } 82 | 83 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool 84 | { 85 | true 86 | } 87 | 88 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 89 | { 90 | let attributes = super.layoutAttributesForElements(in: rect) 91 | 92 | attributes?.forEach 93 | { layoutAttribute in 94 | switch layoutAttribute.representedElementCategory 95 | { 96 | case .cell: 97 | layoutAttributesForItem(at: layoutAttribute.indexPath).map { layoutAttribute.frame = $0.frame } 98 | default: break 99 | } 100 | } 101 | 102 | return attributes 103 | } 104 | 105 | private var leftEdge: CGFloat 106 | { 107 | guard let insets = collectionView?.adjustedContentInset 108 | else 109 | { 110 | return sectionInset.left 111 | } 112 | return insets.left + sectionInset.left 113 | } 114 | 115 | private var contentWidth: CGFloat? 116 | { 117 | guard let collectionViewWidth = collectionView?.frame.size.width, 118 | let insets = collectionView?.adjustedContentInset 119 | else 120 | { 121 | return nil 122 | } 123 | return collectionViewWidth - insets.left - insets.right - sectionInset.left - sectionInset.right 124 | } 125 | 126 | override var collectionViewContentSize: CGSize 127 | { 128 | CGSize(width: contentWidth ?? super.collectionViewContentSize.width, height: super.collectionViewContentSize.height) 129 | } 130 | 131 | fileprivate func isFrame(for firstItemAttributes: UICollectionViewLayoutAttributes, inSameLineAsFrameFor secondItemAttributes: UICollectionViewLayoutAttributes) -> Bool 132 | { 133 | guard let lineWidth = contentWidth 134 | else 135 | { 136 | return false 137 | } 138 | let firstItemFrame = firstItemAttributes.frame 139 | let lineFrame = CGRect( 140 | x: leftEdge, 141 | y: firstItemFrame.origin.y, 142 | width: lineWidth, 143 | height: firstItemFrame.size.height) 144 | return lineFrame.intersects(secondItemAttributes.frame) 145 | } 146 | 147 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? 148 | { 149 | guard let attributes = super.layoutAttributesForItem(at: indexPath) 150 | else 151 | { 152 | return nil 153 | } 154 | guard 155 | indexPath.item > 0, 156 | let previousAttributes = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section)) 157 | else 158 | { 159 | attributes.frame.origin.x = leftEdge // first item of the section should always be left aligned 160 | return attributes 161 | } 162 | 163 | if isFrame(for: attributes, inSameLineAsFrameFor: previousAttributes) 164 | { 165 | attributes.frame.origin.x = previousAttributes.frame.maxX + minimumInteritemSpacing 166 | } 167 | else 168 | { 169 | attributes.frame.origin.x = leftEdge 170 | } 171 | 172 | return attributes 173 | } 174 | } 175 | 176 | struct TagsScreen_Previews: PreviewProvider 177 | { 178 | static var previews: some View 179 | { 180 | TagsScreen() 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Screens/Waterfall/WaterfallScreen.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | import UIKit 6 | 7 | /// THIS IS A WORK IN PROGRESS 8 | struct WaterfallScreen: View 9 | { 10 | @State var data: [[Post]] = (0 ... 10).map { DataSource.postsForWaterfallSection($0, number: 100) } 11 | @State var selectedIndexes: [SectionID: Set] = [:] 12 | @State var selectedPost: Post? = nil // Post being viewed in the detail view 13 | @State var columnMinSize: CGFloat = 150 14 | 15 | @Environment(\.editMode) private var editMode 16 | var isEditing: Bool 17 | { 18 | editMode?.wrappedValue.isEditing ?? false 19 | } 20 | 21 | typealias SectionID = Int 22 | 23 | var sections: [ASCollectionViewSection] 24 | { 25 | data.enumerated().map 26 | { offset, sectionData in 27 | ASCollectionViewSection( 28 | id: offset, 29 | data: sectionData, 30 | selectionMode: self.isEditing ? .selectMultiple($selectedIndexes[offset]) : .selectSingle 31 | { selectedIndex in 32 | selectedPost = sectionData[selectedIndex] 33 | }, 34 | onCellEvent: onCellEvent) 35 | { item, state in 36 | GeometryReader 37 | { geom in 38 | ZStack(alignment: .bottomTrailing) 39 | { 40 | ASRemoteImageView(item.url) 41 | .scaledToFill() 42 | .frame(width: geom.size.width, height: geom.size.height) 43 | .opacity(self.opacity(isHighlighted: state.isHighlighted, isSelected: state.isSelected)) 44 | 45 | if self.isEditing && state.isSelected 46 | { 47 | ZStack 48 | { 49 | Circle() 50 | .fill(Color.blue) 51 | Circle() 52 | .strokeBorder(Color.white, lineWidth: 2) 53 | Image(systemName: "checkmark") 54 | .font(.system(size: 10, weight: .bold)) 55 | .foregroundColor(.white) 56 | } 57 | .frame(width: 20, height: 20) 58 | .padding(10) 59 | } 60 | else 61 | { 62 | Text("\(item.offset)") 63 | .font(.headline) 64 | .bold() 65 | .padding(2) 66 | .background(Color(.systemBackground).opacity(0.5)) 67 | .cornerRadius(4) 68 | .padding(10) 69 | } 70 | } 71 | .frame(width: geom.size.width, height: geom.size.height) 72 | .clipped() 73 | } 74 | }.sectionHeader 75 | { 76 | Text("Section \(offset)") 77 | .padding() 78 | .frame(idealWidth: .infinity, maxWidth: .infinity, idealHeight: .infinity, maxHeight: .infinity, alignment: .leading) 79 | .background(Color.blue) 80 | } 81 | } 82 | } 83 | 84 | var body: some View 85 | { 86 | VStack(spacing: 0) 87 | { 88 | if self.isEditing 89 | { 90 | HStack 91 | { 92 | Text("Min. column size") 93 | Slider(value: self.$columnMinSize, in: 60 ... 200) 94 | }.padding() 95 | } 96 | 97 | ASCollectionView( 98 | editMode: isEditing, 99 | sections: sections) 100 | .layout(self.layout) 101 | .customDelegate(WaterfallScreenLayoutDelegate.init) 102 | .contentInsets(.init(top: 0, left: 10, bottom: 10, right: 10)) 103 | .postSheet(item: $selectedPost, onDismiss: { self.selectedIndexes = [:] }) 104 | .navigationBarTitle("Waterfall Layout", displayMode: .inline) 105 | .navigationBarItems( 106 | trailing: 107 | HStack(spacing: 20) 108 | { 109 | if self.isEditing 110 | { 111 | Button(action: { 112 | withAnimation 113 | { 114 | self.selectedIndexes.forEach 115 | { sectionIndex, selected in 116 | self.data[sectionIndex].remove(atOffsets: IndexSet(selected)) 117 | } 118 | } 119 | }) 120 | { 121 | Image(systemName: "trash") 122 | } 123 | } 124 | 125 | EditButton() 126 | }) 127 | } 128 | } 129 | 130 | func opacity(isHighlighted: Bool, isSelected: Bool) -> Double 131 | { 132 | if !isEditing && isHighlighted 133 | { 134 | return 0.7 135 | } 136 | else if isEditing && isSelected 137 | { 138 | return 0.7 139 | } 140 | else 141 | { 142 | return 1 143 | } 144 | } 145 | 146 | func onCellEvent(_ event: CellEvent) 147 | { 148 | switch event 149 | { 150 | case let .onAppear(item): 151 | ASRemoteImageManager.shared.load(item.url) 152 | case let .onDisappear(item): 153 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 154 | case let .prefetchForData(data): 155 | for item in data 156 | { 157 | ASRemoteImageManager.shared.load(item.url) 158 | } 159 | case let .cancelPrefetchForData(data): 160 | for item in data 161 | { 162 | ASRemoteImageManager.shared.cancelLoad(for: item.url) 163 | } 164 | } 165 | } 166 | } 167 | 168 | private extension View 169 | { 170 | func postSheet(item: Binding, onDismiss: @escaping () -> Void) -> some View 171 | { 172 | sheet(item: item, onDismiss: onDismiss) 173 | { post in 174 | VStack 175 | { 176 | ASRemoteImageView(post.url) 177 | .scaledToFill() 178 | } 179 | } 180 | } 181 | } 182 | 183 | extension WaterfallScreen 184 | { 185 | var layout: ASCollectionLayout 186 | { 187 | ASCollectionLayout(createCustomLayout: ASWaterfallLayout.init) 188 | { layout in 189 | layout.numberOfColumns = .adaptive(minWidth: self.columnMinSize) 190 | } 191 | // Can also initialise like this when no need to dynamically update values 192 | /* 193 | ASCollectionLayout 194 | { 195 | let layout = ASWaterfallLayout() 196 | return layout 197 | } 198 | */ 199 | } 200 | } 201 | 202 | struct WaterfallScreen_Previews: PreviewProvider 203 | { 204 | static var previews: some View 205 | { 206 | WaterfallScreen() 207 | } 208 | } 209 | 210 | class WaterfallScreenLayoutDelegate: ASCollectionViewDelegate, ASWaterfallLayoutDelegate 211 | { 212 | func heightForHeader(sectionIndex: Int) -> CGFloat? 213 | { 214 | 60 215 | } 216 | 217 | /// We explicitely provide a height here. If providing no delegate, this layout will use auto-sizing, however this causes problems if rotating the device (due to limitaitons in UICollecitonView and autosizing cells that are not visible) 218 | func heightForCell(at indexPath: IndexPath, context: ASWaterfallLayout.CellLayoutContext) -> CGFloat 219 | { 220 | guard let post: Post = getDataForItem(at: indexPath) else { return 100 } 221 | return context.width / post.aspectRatio 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/SharedModels/Post.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct Post: Identifiable 7 | { 8 | var username: String 9 | var location: String 10 | var caption: String 11 | var aspectRatio: CGFloat 12 | var randomNumberForImage: Int 13 | var offset: Int 14 | 15 | var url: URL 16 | { 17 | URL(string: "https://picsum.photos/\(Int(aspectRatio * 500))/500?random=\(abs(randomNumberForImage))")! 18 | } 19 | 20 | var usernamePhotoURL = URL(string: "https://picsum.photos/100?random=\(Int.random(in: 0 ... 500))")! 21 | var comments: Int = .random(in: 4 ... 600) 22 | 23 | var id: Int 24 | { 25 | randomNumberForImage.hashValue 26 | } 27 | 28 | static func randomPost(_ randomNumber: Int, aspectRatio: CGFloat, offset: Int = 0) -> Post 29 | { 30 | Post( 31 | username: Lorem.fullName, 32 | location: Lorem.words(Int.random(in: 1 ... 3)), 33 | caption: Lorem.sentences(1 ... 3), 34 | aspectRatio: aspectRatio, 35 | randomNumberForImage: randomNumber, 36 | offset: offset) 37 | } 38 | } 39 | 40 | struct DataSource 41 | { 42 | static func postsForGridSection(_ sectionID: Int, number: Int = 12) -> [Post] 43 | { 44 | (0 ..< number).map 45 | { b -> Post in 46 | let aspect: CGFloat = 1 47 | return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b) 48 | } 49 | } 50 | 51 | static func postsForInstaSection(_ sectionID: Int, number: Int = 5) -> [Post] 52 | { 53 | (0 ..< number).map 54 | { b -> Post in 55 | let aspect: CGFloat = [0.75, 1.0, 1.5].randomElement() ?? 1 56 | return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b) 57 | } 58 | } 59 | 60 | static func postsForWaterfallSection(_ sectionID: Int, number: Int = 12) -> [Post] 61 | { 62 | (0 ..< number).map 63 | { b -> Post in 64 | let aspect: CGFloat = .random(in: 0.3 ... 1.5) 65 | return Post.randomPost(sectionID * 10_000 + b, aspectRatio: aspect, offset: b) 66 | } 67 | } 68 | 69 | static func appsForSection(_ sectionID: Int) -> [App] 70 | { 71 | (0 ... 17).map 72 | { b -> App in 73 | App.randomApp(sectionID * 10_000 + b) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Support/ASCache.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | final class ASCache 6 | { 7 | private let wrappedCache = NSCache() 8 | private let keyTracker = KeyTracker() 9 | 10 | private let entryLifetime: TimeInterval? 11 | 12 | init(entryLifetime: TimeInterval? = nil) 13 | { 14 | self.entryLifetime = entryLifetime 15 | wrappedCache.delegate = keyTracker 16 | } 17 | 18 | subscript(_ key: Key) -> Value? 19 | { 20 | get { value(forKey: key) } 21 | set { setValue(newValue, forKey: key) } 22 | } 23 | 24 | func setValue(_ value: Value?, forKey key: Key) 25 | { 26 | guard let value = value 27 | else 28 | { 29 | removeValue(forKey: key) 30 | return 31 | } 32 | let expirationDate = entryLifetime.map { Date().addingTimeInterval($0) } 33 | let entry = Entry(key: key, value: value, expirationDate: expirationDate) 34 | setEntry(entry) 35 | } 36 | 37 | func value(forKey key: Key) -> Value? 38 | { 39 | entry(forKey: key)?.value 40 | } 41 | 42 | func removeValue(forKey key: Key) 43 | { 44 | wrappedCache.removeObject(forKey: WrappedKey(key)) 45 | } 46 | } 47 | 48 | private extension ASCache 49 | { 50 | func entry(forKey key: Key) -> Entry? 51 | { 52 | guard let entry = wrappedCache.object(forKey: WrappedKey(key)) 53 | else 54 | { 55 | return nil 56 | } 57 | 58 | guard !entry.hasExpired 59 | else 60 | { 61 | removeValue(forKey: key) 62 | return nil 63 | } 64 | 65 | return entry 66 | } 67 | 68 | func setEntry(_ entry: Entry) 69 | { 70 | wrappedCache.setObject(entry, forKey: WrappedKey(entry.key)) 71 | keyTracker.keys.insert(entry.key) 72 | } 73 | } 74 | 75 | private extension ASCache 76 | { 77 | final class KeyTracker: NSObject, NSCacheDelegate 78 | { 79 | var keys = Set() 80 | 81 | func cache( 82 | _ cache: NSCache, 83 | willEvictObject object: Any) 84 | { 85 | guard let entry = object as? Entry 86 | else 87 | { 88 | return 89 | } 90 | 91 | keys.remove(entry.key) 92 | } 93 | } 94 | } 95 | 96 | private extension ASCache 97 | { 98 | final class WrappedKey: NSObject 99 | { 100 | let key: Key 101 | 102 | init(_ key: Key) { self.key = key } 103 | 104 | override var hash: Int { key.hashValue } 105 | 106 | override func isEqual(_ object: Any?) -> Bool 107 | { 108 | guard let value = object as? WrappedKey 109 | else 110 | { 111 | return false 112 | } 113 | 114 | return value.key == key 115 | } 116 | } 117 | 118 | final class Entry 119 | { 120 | let key: Key 121 | let value: Value 122 | let expirationDate: Date? 123 | 124 | var hasExpired: Bool 125 | { 126 | if let expirationDate = expirationDate 127 | { 128 | // Discard values that have expired 129 | return Date() >= expirationDate 130 | } 131 | return false 132 | } 133 | 134 | init(key: Key, value: Value, expirationDate: Date? = nil) 135 | { 136 | self.key = key 137 | self.value = value 138 | self.expirationDate = expirationDate 139 | } 140 | } 141 | } 142 | 143 | extension ASCache.Entry: Codable where Key: Codable, Value: Codable {} 144 | 145 | extension ASCache: Codable where Key: Codable, Value: Codable 146 | { 147 | convenience init(from decoder: Decoder) throws 148 | { 149 | self.init() 150 | 151 | let container = try decoder.singleValueContainer() 152 | let entries = try container.decode([Entry].self) 153 | // Only load non-expired entries 154 | entries.filter { !$0.hasExpired }.forEach(setEntry) 155 | } 156 | 157 | func encode(to encoder: Encoder) throws 158 | { 159 | // Only save non-expired entries 160 | let currentEntries = keyTracker.keys.compactMap(entry).filter { !$0.hasExpired } 161 | var container = encoder.singleValueContainer() 162 | try container.encode(currentEntries) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Support/ASRemoteImageManager.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Combine 4 | import Foundation 5 | import UIKit 6 | 7 | class ASRemoteImageManager 8 | { 9 | static let shared = ASRemoteImageManager() 10 | private init() {} 11 | 12 | let cache = ASCache() 13 | 14 | func load(_ url: URL) 15 | { 16 | imageLoader(for: url).start() 17 | } 18 | 19 | func cancelLoad(for url: URL) 20 | { 21 | imageLoader(for: url).cancel() 22 | } 23 | 24 | func imageLoader(for url: URL) -> ASRemoteImageLoader 25 | { 26 | if let existing = cache.value(forKey: url) 27 | { 28 | return existing 29 | } 30 | else 31 | { 32 | let loader = ASRemoteImageLoader(url) 33 | cache.setValue(loader, forKey: url) 34 | return loader 35 | } 36 | } 37 | } 38 | 39 | class ASRemoteImageLoader: ObservableObject 40 | { 41 | var url: URL 42 | 43 | @Published 44 | var state: State? 45 | { 46 | didSet 47 | { 48 | DispatchQueue.main.async 49 | { 50 | self.stateDidChange.send() 51 | } 52 | } 53 | } 54 | 55 | public let stateDidChange = PassthroughSubject() 56 | 57 | init(_ url: URL) 58 | { 59 | self.url = url 60 | } 61 | 62 | enum State 63 | { 64 | case loading 65 | case success(UIImage) 66 | case failed 67 | } 68 | 69 | private var cancellable: AnyCancellable? 70 | 71 | var image: UIImage? 72 | { 73 | switch state 74 | { 75 | case let .success(image): 76 | return image 77 | default: 78 | return nil 79 | } 80 | } 81 | 82 | func start() 83 | { 84 | guard state == nil 85 | else 86 | { 87 | return 88 | } 89 | DispatchQueue.main.async 90 | { 91 | self.state = .loading 92 | } 93 | 94 | cancellable = URLSession.shared.dataTaskPublisher(for: url) 95 | .map { UIImage(data: $0.data) } 96 | .replaceError(with: nil) 97 | .receive(on: DispatchQueue.main) 98 | .sink 99 | { image in 100 | if let image = image 101 | { 102 | self.state = .success(image) 103 | } 104 | else 105 | { 106 | self.state = .failed 107 | } 108 | } 109 | } 110 | 111 | func cancel() 112 | { 113 | cancellable?.cancel() 114 | cancellable = nil 115 | guard case .success = state 116 | else 117 | { 118 | DispatchQueue.main.async 119 | { 120 | self.state = nil 121 | } 122 | return 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Support/ASRemoteImageView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | struct ASRemoteImageView: View 7 | { 8 | init(_ url: URL) 9 | { 10 | self.url = url 11 | imageLoader = ASRemoteImageManager.shared.imageLoader(for: url) 12 | } 13 | 14 | let url: URL 15 | @ObservedObject 16 | var imageLoader: ASRemoteImageLoader 17 | 18 | var content: some View 19 | { 20 | ZStack 21 | { 22 | Color(.secondarySystemBackground) 23 | Image(systemName: "photo") 24 | self.imageLoader.image.map 25 | { image in 26 | Image(uiImage: image) 27 | .resizable() 28 | }.transition(AnyTransition.opacity.animation(Animation.default)) 29 | } 30 | .compositingGroup() 31 | } 32 | 33 | var body: some View 34 | { 35 | content 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Demo/ASCollectionViewDemo/Support/OnChange.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | extension View 6 | { 7 | func onChange(of value: V, perform action: @escaping (V) -> Void) -> some View 8 | { 9 | OnChange(content: self, value: value, perform: action) 10 | } 11 | } 12 | 13 | // `View.onChange(of:perform:)` is only available on iOS 14.0+ 14 | // https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:) 15 | private struct OnChange: View 16 | { 17 | private let content: Content 18 | private let current: V 19 | private let action: (V) -> Void 20 | 21 | @State private var state: ValueState 22 | 23 | init(content: Content, value: V, perform action: @escaping (V) -> Void) 24 | { 25 | self.content = content 26 | current = value 27 | self.action = action 28 | _state = .init(initialValue: ValueState(value)) 29 | } 30 | 31 | var body: some View 32 | { 33 | if state.didChange(current) 34 | { 35 | DispatchQueue.main.async 36 | { 37 | self.action(self.current) 38 | } 39 | } 40 | return content 41 | } 42 | } 43 | 44 | private final class ValueState 45 | { 46 | private var current: V 47 | 48 | init(_ value: V) 49 | { 50 | current = value 51 | } 52 | 53 | func didChange(_ new: V) -> Bool 54 | { 55 | guard new != current else { return false } 56 | current = new 57 | return true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Demo/BuildTools/BuildTools.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | struct BuildTools 4 | { 5 | var text = "Hello, World!" 6 | } 7 | -------------------------------------------------------------------------------- /Demo/BuildTools/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftFormat", 6 | "repositoryURL": "https://github.com/nicklockwood/SwiftFormat", 7 | "state": { 8 | "branch": null, 9 | "revision": "337d787cae2ac7c53f3574c107d12c557002a883", 10 | "version": "0.46.3" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/BuildTools/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BuildTools", 6 | platforms: [.macOS(.v10_11)], 7 | dependencies: [// Define any tools you want available from your build phases 8 | // Here's an example with SwiftFormat 9 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2")], 10 | targets: [.target(name: "BuildTools", path: "")]) 11 | -------------------------------------------------------------------------------- /Demo/README code content.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | // This file is built with the demo project to ensure the sample code used in the readme is valid 4 | 5 | import ASCollectionView 6 | import SwiftUI 7 | 8 | struct READMEContent 9 | { 10 | // MARK: EXAMPLE 1 11 | 12 | struct SingleSectionExampleView: View 13 | { 14 | @State var dataExample = (0 ..< 30).map { $0 } 15 | 16 | var body: some View 17 | { 18 | ASCollectionView(data: dataExample, dataID: \.self) 19 | { item, _ in 20 | Color.blue 21 | .overlay(Text("\(item)")) 22 | } 23 | .layout 24 | { 25 | .grid( 26 | layoutMode: .adaptive(withMinItemSize: 100), 27 | itemSpacing: 5, 28 | lineSpacing: 5, 29 | itemSize: .absolute(50)) 30 | } 31 | } 32 | } 33 | 34 | // MARK: EXAMPLE 2 35 | 36 | struct ExampleView: View 37 | { 38 | @State var dataExampleA = (0 ..< 21).map { $0 } 39 | @State var dataExampleB = (0 ..< 15).map { "ITEM \($0)" } 40 | 41 | var body: some View 42 | { 43 | ASCollectionView 44 | { 45 | ASCollectionViewSection( 46 | id: 0, 47 | data: dataExampleA, 48 | dataID: \.self) 49 | { item, _ in 50 | Color.blue 51 | .overlay( 52 | Text("\(item)") 53 | ) 54 | } 55 | ASCollectionViewSection( 56 | id: 1, 57 | data: dataExampleB, 58 | dataID: \.self) 59 | { item, _ in 60 | Color.green 61 | .overlay( 62 | Text("Complex layout - \(item)") 63 | ) 64 | } 65 | .sectionHeader 66 | { 67 | Text("Section header") 68 | .padding() 69 | .frame(maxWidth: .infinity, alignment: .leading) 70 | .background(Color.yellow) 71 | } 72 | .sectionFooter 73 | { 74 | Text("This is a section footer!") 75 | .padding() 76 | } 77 | } 78 | .layout 79 | { sectionID in 80 | switch sectionID 81 | { 82 | default: 83 | return .grid( 84 | layoutMode: .adaptive(withMinItemSize: 100), 85 | itemSpacing: 5, 86 | lineSpacing: 5, 87 | itemSize: .absolute(50)) 88 | } 89 | } 90 | } 91 | } 92 | 93 | var sectionHeaderExample: ASCollectionViewSection 94 | { 95 | ASCollectionViewSection(id: 0) 96 | { 97 | Text("Cell 1") 98 | Text("Cell 2") 99 | } 100 | .sectionHeader 101 | { 102 | Text("Section header") 103 | .background(Color.yellow) 104 | } 105 | .sectionFooter 106 | { 107 | Text("Section footer") 108 | .background(Color.blue) 109 | } 110 | .sectionSupplementary(ofKind: "someOtherSupplementaryKindRequestedByYourLayout") 111 | { 112 | Text("Section supplementary") 113 | .background(Color.green) 114 | } 115 | } 116 | 117 | // MARK: DecorationView Example 118 | 119 | struct GroupBackground: View, Decoration 120 | { 121 | let cornerRadius: CGFloat = 12 122 | var body: some View 123 | { 124 | RoundedRectangle(cornerRadius: cornerRadius) 125 | .fill(Color(.secondarySystemGroupedBackground)) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Apptek Studios 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.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "DifferenceKit", 6 | "repositoryURL": "https://github.com/ra1028/DifferenceKit", 7 | "state": { 8 | "branch": null, 9 | "revision": "14c66681e12a38b81045f44c6c29724a0d4b0e72", 10 | "version": "1.1.5" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 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(name: "ASCollectionView", 7 | platforms: [.iOS(.v11)], 8 | products: [// Products define the executables and libraries produced by a package, and make them visible to other packages. 9 | .library(name: "ASCollectionView", 10 | targets: ["ASCollectionView"]), 11 | .library(name: "ASCollectionViewDynamic", type: .dynamic, targets: ["ASCollectionView"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/ra1028/DifferenceKit", .upToNextMajor(from: Version(1, 1, 5))) 15 | ], 16 | targets: [ 17 | .target(name: "ASCollectionView", 18 | dependencies: ["DifferenceKit"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASCellContext.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | /// The context passed to your dynamic section initialiser. Use this to change your view content depending on the context (eg. selected) 6 | @available(iOS 13.0, *) 7 | public struct ASCellContext 8 | { 9 | public var isHighlighted: Bool 10 | public var isSelected: Bool 11 | public var index: Int 12 | public var isFirstInSection: Bool 13 | public var isLastInSection: Bool 14 | } 15 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASCollectionView+Initialisers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | // MARK: Init for multi-section CVs 7 | 8 | @available(iOS 13.0, *) 9 | public extension ASCollectionView 10 | { 11 | /** 12 | Initializes a collection view with the given sections 13 | 14 | - Parameters: 15 | - sections: An array of sections (ASCollectionViewSection) 16 | */ 17 | init(editMode: Bool = false, sections: [Section]) 18 | { 19 | self.editMode = editMode 20 | self.sections = sections 21 | } 22 | 23 | /** 24 | Initializes a collection view with the given sections 25 | 26 | - Parameters: 27 | - sectionBuilder: A closure containing multiple sections (ASCollectionViewSection) 28 | */ 29 | init(editMode: Bool = false, @SectionArrayBuilder sectionBuilder: () -> [Section]) 30 | { 31 | sections = sectionBuilder() 32 | } 33 | } 34 | 35 | // MARK: Init for single-section CV 36 | 37 | @available(iOS 13.0, *) 38 | public extension ASCollectionView where SectionID == Int 39 | { 40 | /** 41 | Initializes a collection view with a single section. 42 | 43 | - Parameters: 44 | - section: A single section (ASCollectionViewSection) 45 | */ 46 | init(editMode: Bool = false, section: Section) 47 | { 48 | self.editMode = editMode 49 | sections = [section] 50 | } 51 | 52 | /** 53 | Initializes a collection view with a single section of static content 54 | */ 55 | init(editMode: Bool = false, @ViewArrayBuilder staticContent: () -> ViewArrayBuilder.Wrapper) 56 | { 57 | self.init(editMode: editMode, sections: [ASCollectionViewSection(id: 0, content: staticContent)]) 58 | } 59 | 60 | /** 61 | Initializes a collection view with a single section. 62 | */ 63 | init( 64 | editMode: Bool = false, 65 | data: DataCollection, 66 | dataID dataIDKeyPath: KeyPath, 67 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 68 | where DataCollection.Index == Int 69 | { 70 | self.editMode = editMode 71 | let section = ASCollectionViewSection( 72 | id: 0, 73 | data: data, 74 | dataID: dataIDKeyPath, 75 | contentBuilder: contentBuilder) 76 | sections = [section] 77 | } 78 | 79 | /** 80 | Initializes a collection view with a single section with identifiable data 81 | */ 82 | init( 83 | editMode: Bool = false, 84 | data: DataCollection, 85 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 86 | where DataCollection.Index == Int, DataCollection.Element: Identifiable 87 | { 88 | self.init(editMode: editMode, data: data, dataID: \.id, contentBuilder: contentBuilder) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASCollectionView+Modifiers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import UIKit 4 | import SwiftUI 5 | 6 | // MARK: Modifer: Custom Delegate 7 | 8 | @available(iOS 13.0, *) 9 | public extension ASCollectionView 10 | { 11 | /// Use this modifier to assign a custom delegate type (subclass of ASCollectionViewDelegate). This allows support for old UICollectionViewLayouts that require a delegate. 12 | func customDelegate(_ delegateInitialiser: @escaping (() -> ASCollectionViewDelegate)) -> Self 13 | { 14 | var cv = self 15 | cv.delegateInitialiser = delegateInitialiser 16 | return cv 17 | } 18 | } 19 | 20 | // MARK: Modifer: Layout Invalidation 21 | 22 | @available(iOS 13.0, *) 23 | public extension ASCollectionView 24 | { 25 | /// For use in cases where you would like to change layout settings in response to a change in variables referenced by your layout closure. 26 | /// Note: this ensures the layout is invalidated 27 | /// - For UICollectionViewCompositionalLayout this means that your SectionLayout closure will be called again 28 | /// - closures capture value types when created, therefore you must refer to a reference type in your layout closure if you want it to update. 29 | func shouldInvalidateLayoutOnStateChange(_ shouldInvalidate: Bool, animated: Bool = true) -> Self 30 | { 31 | var this = self 32 | this.shouldInvalidateLayoutOnStateChange = shouldInvalidate 33 | this.shouldAnimateInvalidatedLayoutOnStateChange = animated 34 | return this 35 | } 36 | 37 | /// For use in cases where you would like to recreate the layout object in response to a change in state. Eg. for changing layout types completely 38 | /// If not changing the type of layout (eg. to a different class) t is preferable to invalidate the layout and update variables in the `configureCustomLayout` closure 39 | func shouldRecreateLayoutOnStateChange(_ shouldRecreate: Bool, animated: Bool = true) -> Self 40 | { 41 | var this = self 42 | this.shouldRecreateLayoutOnStateChange = shouldRecreate 43 | this.shouldAnimateRecreatedLayoutOnStateChange = animated 44 | return this 45 | } 46 | } 47 | 48 | // MARK: Modifer: Other Modifiers 49 | 50 | @available(iOS 13.0, *) 51 | public extension ASCollectionView 52 | { 53 | /// Set a closure that is called whenever the collectionView is scrolled 54 | func onScroll(_ onScroll: @escaping OnScrollCallback) -> Self 55 | { 56 | var this = self 57 | this.onScrollCallback = onScroll 58 | return this 59 | } 60 | 61 | /// Set a closure that is called whenever the collectionView is scrolled to a boundary. eg. the bottom. 62 | /// This is useful to enable loading more data when scrolling to bottom 63 | func onReachedBoundary(_ onReachedBoundary: @escaping OnReachedBoundaryCallback) -> Self 64 | { 65 | var this = self 66 | this.onReachedBoundaryCallback = onReachedBoundary 67 | return this 68 | } 69 | 70 | /// Sets the collection view's background color 71 | func backgroundColor(_ color: UIColor?) -> Self 72 | { 73 | var this = self 74 | this.backgroundColor = color 75 | return this 76 | } 77 | 78 | /// Set whether to show scroll indicators 79 | func scrollIndicatorsEnabled(horizontal: Bool = true, vertical: Bool = true) -> Self 80 | { 81 | var this = self 82 | this.horizontalScrollIndicatorEnabled = horizontal 83 | this.verticalScrollIndicatorEnabled = vertical 84 | return this 85 | } 86 | 87 | /// Set the content insets 88 | func contentInsets(_ insets: UIEdgeInsets) -> Self 89 | { 90 | var this = self 91 | this.contentInsets = insets 92 | return this 93 | } 94 | 95 | /// Set a closure that is called when the collectionView will display a cell 96 | func onWillDisplay(_ callback: ((UICollectionViewCell, IndexPath)->Void)?) -> Self 97 | { 98 | var this = self 99 | this.onWillDisplay = callback 100 | return this 101 | } 102 | 103 | /// Set a closure that is called when the collectionView did display a cell 104 | func onDidDisplay(_ callback: ((UICollectionViewCell, IndexPath)->Void)?) -> Self 105 | { 106 | var this = self 107 | this.onDidDisplay = callback 108 | return this 109 | } 110 | 111 | /// Set a closure that is called when the collectionView is pulled to refresh 112 | func onPullToRefresh(_ callback: ((_ endRefreshing: @escaping (() -> Void)) -> Void)?) -> Self 113 | { 114 | var this = self 115 | this.onPullToRefresh = callback 116 | return this 117 | } 118 | 119 | /// Set whether the ASCollectionView should always allow bounce vertically 120 | func alwaysBounceVertical(_ alwaysBounce: Bool = true) -> Self 121 | { 122 | var this = self 123 | this.alwaysBounceVertical = alwaysBounce 124 | return this 125 | } 126 | 127 | /// Set whether the ASCollectionView should always allow bounce horizontally 128 | func alwaysBounceHorizontal(_ alwaysBounce: Bool = true) -> Self 129 | { 130 | var this = self 131 | this.alwaysBounceHorizontal = alwaysBounce 132 | return this 133 | } 134 | 135 | /// Set a binding that will scroll the ASCollectionView when set. It will always return nil once the scroll is applied (use onScroll to read scroll position) 136 | func scrollPositionSetter(_ binding: Binding) -> Self 137 | { 138 | var this = self 139 | _ = binding.wrappedValue // Touch the binding so that SwiftUI will notify us of future updates 140 | this.scrollPositionSetter = binding 141 | return this 142 | } 143 | 144 | /// Set whether the ASCollectionView should animate on data refresh 145 | func animateOnDataRefresh(_ animate: Bool = true) -> Self 146 | { 147 | var this = self 148 | this.animateOnDataRefresh = animate 149 | return this 150 | } 151 | 152 | /// Set whether the ASCollectionView should attempt to maintain scroll position on orientation change, default is true 153 | func shouldAttemptToMaintainScrollPositionOnOrientationChange(maintainPosition: Bool) -> Self 154 | { 155 | var this = self 156 | this.maintainScrollPositionOnOrientationChange = maintainPosition 157 | return this 158 | } 159 | 160 | /// Set whether the ASCollectionView should automatically scroll an active textview/input to avoid the system keyboard. Default is true 161 | func shouldScrollToAvoidKeyboard(_ avoidKeyboard: Bool = true) -> Self 162 | { 163 | var this = self 164 | this.dodgeKeyboard = avoidKeyboard 165 | return this 166 | } 167 | } 168 | 169 | // MARK: PUBLIC layout modifier functions 170 | 171 | @available(iOS 13.0, *) 172 | public extension ASCollectionView 173 | { 174 | func layout(_ layout: Layout) -> Self 175 | { 176 | var this = self 177 | this.layout = layout 178 | return this 179 | } 180 | 181 | func layout( 182 | scrollDirection: UICollectionView.ScrollDirection = .vertical, 183 | interSectionSpacing: CGFloat = 10, 184 | layoutPerSection: @escaping CompositionalLayout) -> Self 185 | { 186 | var this = self 187 | this.layout = Layout( 188 | scrollDirection: scrollDirection, 189 | interSectionSpacing: interSectionSpacing, 190 | layoutPerSection: layoutPerSection) 191 | return this 192 | } 193 | 194 | func layout( 195 | scrollDirection: UICollectionView.ScrollDirection = .vertical, 196 | interSectionSpacing: CGFloat = 10, 197 | layout: @escaping CompositionalLayoutIgnoringSections) -> Self 198 | { 199 | var this = self 200 | this.layout = Layout( 201 | scrollDirection: scrollDirection, 202 | interSectionSpacing: interSectionSpacing, 203 | layout: layout) 204 | return this 205 | } 206 | 207 | func layout(customLayout: @escaping (() -> UICollectionViewLayout)) -> Self 208 | { 209 | var this = self 210 | this.layout = Layout(customLayout: customLayout) 211 | return this 212 | } 213 | 214 | func layout(createCustomLayout: @escaping (() -> LayoutClass), configureCustomLayout: @escaping ((LayoutClass) -> Void)) -> Self 215 | { 216 | var this = self 217 | this.layout = Layout(createCustomLayout: createCustomLayout, configureCustomLayout: configureCustomLayout) 218 | return this 219 | } 220 | } 221 | 222 | @available(iOS 13.0, *) 223 | public extension ASCollectionView 224 | { 225 | func shrinkToContentSize(isEnabled: Bool = true, dimension: ShrinkDimension) -> some View 226 | { 227 | SelfSizingWrapper(content: self, shrinkDirection: dimension, isEnabled: isEnabled) 228 | } 229 | 230 | func fitContentSize(isEnabled: Bool = true, dimension: ShrinkDimension) -> some View 231 | { 232 | SelfSizingWrapper(content: self, shrinkDirection: dimension, isEnabled: isEnabled, expandToFitMode: true) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASDragDropConfig+Public.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | public extension ASDragDropConfig 9 | { 10 | /// This provides automatic support for drag/drop/reorder of items in the section 11 | /// It automatically applies changes using the data binding 12 | /// Use the modifiers to add extra checks (eg .canDragItem, .canMoveItem, .dragItemProvider, .dropItemProvider) 13 | init(dataBinding: Binding<[Data]>, dragEnabled: Bool = true, dropEnabled: Bool = true, reorderingEnabled: Bool = true) 14 | { 15 | self.dataBinding = dataBinding 16 | self.dragEnabled = dragEnabled 17 | self.dropEnabled = dropEnabled 18 | self.reorderingEnabled = reorderingEnabled 19 | } 20 | 21 | /// This allows you to manually implement drag/drop/reordering support 22 | /// In the onDelete/onInsert/onMove closures return true if you apply the suggested delete/insert/move, or false if it shouldn't be applied... so that ASCollectionView can correctly animate. 23 | /// Use the modifiers to add extra checks (eg .canDragItem, .canMoveItem, .dragItemProvider, .dropItemProvider) 24 | init(dragEnabled: Bool = true, 25 | dropEnabled: Bool = true, 26 | reorderingEnabled: Bool = true, 27 | onDeleteOrRemoveItems: ((_ indexSet: IndexSet) -> Bool)? = nil, 28 | onInsertItems: ((_ index: Int, _ items: [Data]) -> Bool)? = nil, 29 | onMoveItem: ((Int, Int) -> Bool)? = nil) 30 | { 31 | dataBinding = nil 32 | self.onDeleteOrRemoveItems = onDeleteOrRemoveItems 33 | self.onInsertItems = onInsertItems 34 | self.onMoveItem = onMoveItem 35 | self.dragEnabled = dragEnabled 36 | self.dropEnabled = dropEnabled 37 | self.reorderingEnabled = reorderingEnabled 38 | } 39 | 40 | static var disabled: ASDragDropConfig 41 | { 42 | ASDragDropConfig() 43 | } 44 | 45 | /// Called to check whether an item can be dragged 46 | func canDragItem(_ closure: @escaping ((IndexPath) -> Bool)) -> Self 47 | { 48 | var this = self 49 | this.canDragItem = closure 50 | return this 51 | } 52 | 53 | /// Called to check whether a move should be allowed 54 | func canMoveItem(_ closure: @escaping ((IndexPath, IndexPath) -> Bool)) -> Self 55 | { 56 | var this = self 57 | this.canMoveItem = closure 58 | return this 59 | } 60 | 61 | /// Called to check whether an item can be dropped 62 | func canDropItem(_ closure: @escaping ((IndexPath) -> Bool)) -> Self 63 | { 64 | var this = self 65 | this.canDropItem = closure 66 | return this 67 | } 68 | 69 | /// An optional closure that you can use to decide what to do with a dropped item. 70 | /// Return nil if you want to ignore the drop. 71 | /// Return an item (of the same type as your section data) if you want to insert a row. 72 | /// `sourceItem`: If the drop originated from a cell with the same data source, this will provide the original item that has been dragged 73 | /// `dragItem`: This is the further information provided by UIKit. For example, if a drag came from another app, you could deal with that using this. 74 | func dropItemProvider(_ provider: @escaping ((Data?, UIDragItem) -> Data?)) -> Self 75 | { 76 | var this = self 77 | this.dropEnabled = true 78 | this.dropItemProvider = provider 79 | return this 80 | } 81 | 82 | /// An optional closure that you can use to provide extra info (eg. for dragging outside of your app) 83 | func dragItemProvider(_ provider: @escaping ((_ item: Data) -> NSItemProvider?)) -> Self 84 | { 85 | var this = self 86 | this.dragEnabled = true 87 | this.dragItemProvider = provider 88 | return this 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASSection+Initialisers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | // MARK: DYNAMIC CONTENT SECTION 7 | 8 | @available(iOS 13.0, *) 9 | public extension ASSection 10 | { 11 | /** 12 | Initializes a section with data 13 | 14 | - Parameters: 15 | - id: The id for this section 16 | - data: The data to display in the section. This initialiser expects data that conforms to 'Identifiable' 17 | - dataID: The keypath to a hashable identifier of each data item 18 | - onCellEvent: Use this to respond to cell appearance/disappearance, and preloading events. 19 | - onDragDropEvent: Define this closure to enable drag/drop and respond to events (default is nil: drag/drop disabled) 20 | - contentBuilder: A closure returning a SwiftUI view for the given data item 21 | */ 22 | init( 23 | id: SectionID, 24 | data: DataCollection, 25 | dataID dataIDKeyPath: KeyPath, 26 | container: @escaping ((Content, ASCellContext) -> Container), 27 | selectionMode: ASSectionSelectionMode = .none, 28 | shouldAllowHighlight: ((_ index: Int) -> Bool)? = nil, 29 | shouldAllowSelection: ((_ index: Int) -> Bool)? = nil, 30 | shouldAllowDeselection: ((_ index: Int) -> Bool)? = nil, 31 | onCellEvent: OnCellEvent? = nil, 32 | dragDropConfig: ASDragDropConfig = .disabled, 33 | shouldAllowSwipeToDelete: ShouldAllowSwipeToDelete? = nil, 34 | onSwipeToDelete: OnSwipeToDelete? = nil, 35 | contextMenuProvider: ContextMenuProvider? = nil, 36 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 37 | where DataCollection.Index == Int 38 | { 39 | self.id = id 40 | dataSource = ASSectionDataSource( 41 | data: data, 42 | dataIDKeyPath: dataIDKeyPath, 43 | container: container, 44 | content: contentBuilder, 45 | selectionMode: selectionMode, 46 | shouldAllowHighlight: shouldAllowHighlight, 47 | shouldAllowSelection: shouldAllowSelection, 48 | shouldAllowDeselection: shouldAllowDeselection, 49 | onCellEvent: onCellEvent, 50 | dragDropConfig: dragDropConfig, 51 | shouldAllowSwipeToDelete: shouldAllowSwipeToDelete, 52 | onSwipeToDelete: onSwipeToDelete, 53 | contextMenuProvider: contextMenuProvider) 54 | } 55 | 56 | init( 57 | id: SectionID, 58 | data: DataCollection, 59 | dataID dataIDKeyPath: KeyPath, 60 | selectionMode: ASSectionSelectionMode = .none, 61 | shouldAllowHighlight: ((_ index: Int) -> Bool)? = nil, 62 | shouldAllowSelection: ((_ index: Int) -> Bool)? = nil, 63 | shouldAllowDeselection: ((_ index: Int) -> Bool)? = nil, 64 | onCellEvent: OnCellEvent? = nil, 65 | dragDropConfig: ASDragDropConfig = .disabled, 66 | shouldAllowSwipeToDelete: ShouldAllowSwipeToDelete? = nil, 67 | onSwipeToDelete: OnSwipeToDelete? = nil, 68 | contextMenuProvider: ContextMenuProvider? = nil, 69 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 70 | where DataCollection.Index == Int 71 | { 72 | self.init(id: id, data: data, dataID: dataIDKeyPath, container: { content, _ in content }, selectionMode: selectionMode, shouldAllowHighlight: shouldAllowHighlight, shouldAllowSelection: shouldAllowSelection, shouldAllowDeselection: shouldAllowDeselection, onCellEvent: onCellEvent, dragDropConfig: dragDropConfig, shouldAllowSwipeToDelete: shouldAllowSwipeToDelete, onSwipeToDelete: onSwipeToDelete, contextMenuProvider: contextMenuProvider, contentBuilder: contentBuilder) 73 | } 74 | } 75 | 76 | // MARK: IDENTIFIABLE DATA SECTION 77 | 78 | @available(iOS 13.0, *) 79 | public extension ASCollectionViewSection 80 | { 81 | /** 82 | Initializes a section with identifiable data 83 | - Parameters: 84 | - id: The id for this section 85 | - data: The data to display in the section. This initialiser expects data that conforms to 'Identifiable' 86 | - onCellEvent: Use this to respond to cell appearance/disappearance, and preloading events. 87 | - onDragDropEvent: Define this closure to enable drag/drop and respond to events (default is nil: drag/drop disabled) 88 | - contentBuilder: A closure returning a SwiftUI view for the given data item 89 | */ 90 | init( 91 | id: SectionID, 92 | data: DataCollection, 93 | container: @escaping ((Content, ASCellContext) -> Container), 94 | selectionMode: ASSectionSelectionMode = .none, 95 | shouldAllowHighlight: ((_ index: Int) -> Bool)? = nil, 96 | shouldAllowSelection: ((_ index: Int) -> Bool)? = nil, 97 | shouldAllowDeselection: ((_ index: Int) -> Bool)? = nil, 98 | onCellEvent: OnCellEvent? = nil, 99 | dragDropConfig: ASDragDropConfig = .disabled, 100 | shouldAllowSwipeToDelete: ShouldAllowSwipeToDelete? = nil, 101 | onSwipeToDelete: OnSwipeToDelete? = nil, 102 | contextMenuProvider: ContextMenuProvider? = nil, 103 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 104 | where DataCollection.Index == Int, DataCollection.Element: Identifiable 105 | { 106 | self.init(id: id, data: data, dataID: \.id, container: container, selectionMode: selectionMode, shouldAllowHighlight: shouldAllowHighlight, shouldAllowSelection: shouldAllowSelection, shouldAllowDeselection: shouldAllowDeselection, onCellEvent: onCellEvent, dragDropConfig: dragDropConfig, shouldAllowSwipeToDelete: shouldAllowSwipeToDelete, onSwipeToDelete: onSwipeToDelete, contextMenuProvider: contextMenuProvider, contentBuilder: contentBuilder) 107 | } 108 | 109 | init( 110 | id: SectionID, 111 | data: DataCollection, 112 | selectionMode: ASSectionSelectionMode = .none, 113 | shouldAllowHighlight: ((_ index: Int) -> Bool)? = nil, 114 | shouldAllowSelection: ((_ index: Int) -> Bool)? = nil, 115 | shouldAllowDeselection: ((_ index: Int) -> Bool)? = nil, 116 | onCellEvent: OnCellEvent? = nil, 117 | dragDropConfig: ASDragDropConfig = .disabled, 118 | shouldAllowSwipeToDelete: ShouldAllowSwipeToDelete? = nil, 119 | onSwipeToDelete: OnSwipeToDelete? = nil, 120 | contextMenuProvider: ContextMenuProvider? = nil, 121 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 122 | where DataCollection.Index == Int, DataCollection.Element: Identifiable 123 | { 124 | self.init(id: id, data: data, container: { content, _ in content }, selectionMode: selectionMode, shouldAllowHighlight: shouldAllowHighlight, shouldAllowSelection: shouldAllowSelection, shouldAllowDeselection: shouldAllowDeselection, onCellEvent: onCellEvent, dragDropConfig: dragDropConfig, shouldAllowSwipeToDelete: shouldAllowSwipeToDelete, onSwipeToDelete: onSwipeToDelete, contextMenuProvider: contextMenuProvider, contentBuilder: contentBuilder) 125 | } 126 | } 127 | 128 | // MARK: STATIC CONTENT SECTION 129 | 130 | @available(iOS 13.0, *) 131 | public extension ASCollectionViewSection 132 | { 133 | /** 134 | Initializes a section with static content 135 | 136 | - Parameters: 137 | - id: The id for this section 138 | - content: A closure returning a number of SwiftUI views to display in the collection view 139 | */ 140 | init(id: SectionID, container: @escaping ((AnyView, ASCellContext) -> Container), @ViewArrayBuilder content: () -> ViewArrayBuilder.Wrapper) 141 | { 142 | self.id = id 143 | dataSource = ASSectionDataSource<[ASCollectionViewStaticContent], ASCollectionViewStaticContent.ID, AnyView, Container>( 144 | data: content().flattened().enumerated().map 145 | { 146 | ASCollectionViewStaticContent(index: $0.offset, view: $0.element) 147 | }, 148 | dataIDKeyPath: \.id, 149 | container: container, 150 | content: { staticContent, _ in staticContent.view }, 151 | dragDropConfig: .disabled) 152 | } 153 | 154 | init(id: SectionID, @ViewArrayBuilder content: () -> ViewArrayBuilder.Wrapper) 155 | { 156 | self.init(id: id, container: { content, _ in content }, content: content) 157 | } 158 | 159 | /** 160 | Initializes a section with a single view 161 | 162 | - Parameters: 163 | - id: The id for this section 164 | - content: A single SwiftUI views to display in the collection view 165 | */ 166 | init(id: SectionID, container: @escaping ((AnyView, ASCellContext) -> Container), content: () -> Content) 167 | { 168 | self.id = id 169 | dataSource = ASSectionDataSource<[ASCollectionViewStaticContent], ASCollectionViewStaticContent.ID, AnyView, Container>( 170 | data: [ASCollectionViewStaticContent(index: 0, view: AnyView(content()))], 171 | dataIDKeyPath: \.id, 172 | container: container, 173 | content: { staticContent, _ in staticContent.view }, 174 | dragDropConfig: .disabled) 175 | } 176 | 177 | init(id: SectionID, content: () -> Content) 178 | { 179 | self.init(id: id, container: { content, _ in content }, content: content) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASSection+Modifiers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | // MARK: SUPPLEMENTARY VIEWS - PUBLIC MODIFIERS 7 | 8 | @available(iOS 13.0, *) 9 | public extension ASCollectionViewSection 10 | { 11 | func sectionHeader(content: () -> Content?) -> Self 12 | { 13 | var section = self 14 | section.setHeaderView(content()) 15 | return section 16 | } 17 | 18 | func sectionFooter(content: () -> Content?) -> Self 19 | { 20 | var section = self 21 | section.setFooterView(content()) 22 | return section 23 | } 24 | 25 | func sectionSupplementary(ofKind kind: String, content: () -> Content?) -> Self 26 | { 27 | var section = self 28 | section.setSupplementaryView(content(), ofKind: kind) 29 | return section 30 | } 31 | 32 | func tableViewSetEstimatedSizes(headerHeight: CGFloat? = nil, footerHeight: CGFloat? = nil) -> Self 33 | { 34 | var section = self 35 | section.estimatedHeaderHeight = headerHeight 36 | section.estimatedFooterHeight = footerHeight 37 | return section 38 | } 39 | 40 | func tableViewDisableDefaultTheming() -> Self 41 | { 42 | var section = self 43 | section.disableDefaultTheming = true 44 | return section 45 | } 46 | 47 | func tableViewSeparatorInsets(leading: CGFloat = 0, trailing: CGFloat = 0) -> Self 48 | { 49 | var section = self 50 | section.tableViewSeparatorInsets = UIEdgeInsets(top: 0, left: leading, bottom: 0, right: trailing) 51 | return section 52 | } 53 | 54 | func sectionIndexTitle(_ title: String?) -> Self 55 | { 56 | var section = self 57 | section.sectionIndexTitle = title 58 | return section 59 | } 60 | 61 | // Use this modifier to make a section's cells be cached even when off-screen. This is useful for cells containing nested collection views 62 | func cacheCells() -> Self 63 | { 64 | var section = self 65 | section.shouldCacheCells = true 66 | return section 67 | } 68 | 69 | // MARK: Self-sizing config 70 | 71 | func selfSizingConfig(_ config: @escaping SelfSizingConfig) -> Self 72 | { 73 | var section = self 74 | section.dataSource.setSelfSizingConfig(config: config) 75 | return section 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASTableView+Initialisers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public extension ASTableView 8 | { 9 | /** 10 | Initializes a table view with the given sections 11 | 12 | - Parameters: 13 | - sections: An array of sections (ASTableViewSection) 14 | */ 15 | @inlinable init(style: UITableView.Style = .plain, editMode: Bool = false, sections: [Section]) 16 | { 17 | self.style = style 18 | self.editMode = editMode 19 | self.sections = sections 20 | } 21 | 22 | @inlinable init(style: UITableView.Style = .plain, editMode: Bool = false, @SectionArrayBuilder sectionBuilder: () -> [Section]) 23 | { 24 | self.style = style 25 | self.editMode = editMode 26 | sections = sectionBuilder() 27 | } 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | public extension ASTableView where SectionID == Int 32 | { 33 | /** 34 | Initializes a table view with a single section. 35 | 36 | - Parameters: 37 | - section: A single section (ASTableViewSection) 38 | */ 39 | init(style: UITableView.Style = .plain, editMode: Bool = false, section: Section) 40 | { 41 | self.style = style 42 | self.editMode = editMode 43 | sections = [section] 44 | } 45 | 46 | /** 47 | Initializes a table view with a single section. 48 | */ 49 | init( 50 | style: UITableView.Style = .plain, 51 | editMode: Bool = false, 52 | data: DataCollection, 53 | dataID dataIDKeyPath: KeyPath, 54 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 55 | where DataCollection.Index == Int 56 | { 57 | self.style = style 58 | self.editMode = editMode 59 | let section = ASSection( 60 | id: 0, 61 | data: data, 62 | dataID: dataIDKeyPath, 63 | contentBuilder: contentBuilder) 64 | sections = [section] 65 | } 66 | 67 | /** 68 | Initializes a table view with a single section of identifiable data 69 | */ 70 | init( 71 | style: UITableView.Style = .plain, 72 | editMode: Bool = false, 73 | data: DataCollection, 74 | @ViewBuilder contentBuilder: @escaping ((DataCollection.Element, ASCellContext) -> Content)) 75 | where DataCollection.Index == Int, DataCollection.Element: Identifiable 76 | { 77 | self.init(style: style, editMode: editMode, data: data, dataID: \.id, contentBuilder: contentBuilder) 78 | } 79 | 80 | /** 81 | Initializes a table view with a single section of static content 82 | */ 83 | static func `static`(editMode: Bool = false, @ViewArrayBuilder staticContent: () -> ViewArrayBuilder.Wrapper) -> ASTableView 84 | { 85 | ASTableView( 86 | style: .plain, 87 | editMode: editMode, 88 | sections: [ASTableViewSection(id: 0, content: staticContent)]) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/ASTableView+Modifiers.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | // MARK: PUBLIC Modifier: OnScroll / OnReachedBottom 7 | 8 | @available(iOS 13.0, *) 9 | public extension ASTableView 10 | { 11 | /// Set a closure that is called whenever the tableView is scrolled 12 | func onScroll(_ onScroll: @escaping OnScrollCallback) -> Self 13 | { 14 | var this = self 15 | this.onScrollCallback = onScroll 16 | return this 17 | } 18 | 19 | /// Set a closure that is called whenever the tableView is scrolled to the bottom. 20 | /// This is useful to enable loading more data when scrolling to bottom 21 | func onReachedBottom(_ onReachedBottom: @escaping OnReachedBottomCallback) -> Self 22 | { 23 | var this = self 24 | this.onReachedBottomCallback = onReachedBottom 25 | return this 26 | } 27 | 28 | /// Set whether to show separators between cells 29 | func separatorsEnabled(_ isEnabled: Bool = true) -> Self 30 | { 31 | var this = self 32 | this.separatorsEnabled = isEnabled 33 | return this 34 | } 35 | 36 | /// Set whether to show scroll indicator 37 | func scrollIndicatorEnabled(_ isEnabled: Bool = true) -> Self 38 | { 39 | var this = self 40 | this.scrollIndicatorEnabled = isEnabled 41 | return this 42 | } 43 | 44 | /// Set the content insets 45 | func contentInsets(_ insets: UIEdgeInsets) -> Self 46 | { 47 | var this = self 48 | this.contentInsets = insets 49 | return this 50 | } 51 | 52 | /// Set a closure that is called when the collectionView will display a cell 53 | func onWillDisplay(_ callback: ((UITableViewCell, IndexPath)->Void)?) -> Self 54 | { 55 | var this = self 56 | this.onWillDisplay = callback 57 | return this 58 | } 59 | 60 | /// Set a closure that is called when the collectionView did display a cell 61 | func onDidDisplay(_ callback: ((UITableViewCell, IndexPath)->Void)?) -> Self 62 | { 63 | var this = self 64 | this.onDidDisplay = callback 65 | return this 66 | } 67 | 68 | /// Set a closure that is called when the tableView is pulled to refresh 69 | func onPullToRefresh(_ callback: ((_ endRefreshing: @escaping (() -> Void)) -> Void)?) -> Self 70 | { 71 | var this = self 72 | this.onPullToRefresh = callback 73 | return this 74 | } 75 | 76 | /// Set whether the TableView should always allow bounce vertically 77 | func alwaysBounce(_ alwaysBounce: Bool = true) -> Self 78 | { 79 | var this = self 80 | this.alwaysBounce = alwaysBounce 81 | return this 82 | } 83 | 84 | /// Set whether the TableView should animate on data refresh 85 | func animateOnDataRefresh(_ animate: Bool = true) -> Self 86 | { 87 | var this = self 88 | this.animateOnDataRefresh = animate 89 | return this 90 | } 91 | 92 | /// Set a binding that will scroll the ASTableView when set. It will always return nil once the scroll is applied (use onScroll to read scroll position) 93 | func scrollPositionSetter(_ binding: Binding) -> Self 94 | { 95 | var this = self 96 | _ = binding.wrappedValue // Touch the binding so that SwiftUI will notify us of future updates 97 | this.scrollPositionSetter = binding 98 | return this 99 | } 100 | 101 | /// Set whether the TableView should handle keyboard appearance and disappearance 102 | func shouldHandleKeyboardAppearance(_ isEnabled: Bool) -> Self 103 | { 104 | var this = self 105 | this.shouldHandleKeyboardAppereance = isEnabled 106 | return this 107 | } 108 | } 109 | 110 | // MARK: ASTableView specific header modifiers 111 | 112 | @available(iOS 13.0, *) 113 | public extension ASTableViewSection 114 | { 115 | func sectionHeaderInsetGrouped(content: () -> Content?) -> Self 116 | { 117 | if let content = content() 118 | { 119 | var section = self 120 | let insetGroupedContent = 121 | content 122 | .font(.headline) 123 | .frame(maxWidth: .infinity, alignment: .leading) 124 | .padding(EdgeInsets(top: 12, leading: 0, bottom: 6, trailing: 0)) 125 | 126 | section.setHeaderView(insetGroupedContent) 127 | return section 128 | } 129 | else 130 | { 131 | return self 132 | } 133 | } 134 | } 135 | 136 | @available(iOS 13.0, *) 137 | public extension ASTableView 138 | { 139 | func shrinkToContentSize(isEnabled: Bool = true) -> some View 140 | { 141 | SelfSizingWrapper(content: self, shrinkDirection: .vertical, isEnabled: isEnabled) 142 | } 143 | 144 | func fitContentSize(isEnabled: Bool = true) -> some View 145 | { 146 | SelfSizingWrapper(content: self, shrinkDirection: .vertical, isEnabled: isEnabled, expandToFitMode: true) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASCollectionViewCell: UICollectionViewCell, ASDataSourceConfigurableCell 9 | { 10 | var itemID: ASCollectionViewItemUniqueID? 11 | let hostingController = ASHostingController(AnyView(EmptyView())) 12 | // var skipNextRefresh: Bool = false 13 | 14 | override init(frame: CGRect) 15 | { 16 | super.init(frame: frame) 17 | contentView.addSubview(hostingController.viewController.view) 18 | hostingController.viewController.view.frame = contentView.bounds 19 | } 20 | 21 | @available(*, unavailable) 22 | required init?(coder: NSCoder) 23 | { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | weak var collectionViewController: AS_CollectionViewController? 28 | { 29 | didSet 30 | { 31 | if collectionViewController != oldValue 32 | { 33 | collectionViewController?.addChild(hostingController.viewController) 34 | hostingController.viewController.didMove(toParent: collectionViewController) 35 | } 36 | } 37 | } 38 | 39 | var selfSizingConfig: ASSelfSizingConfig = .init(selfSizeHorizontally: true, selfSizeVertically: true) 40 | 41 | override func prepareForReuse() 42 | { 43 | super.prepareForReuse() 44 | itemID = nil 45 | isSelected = false 46 | alpha = 1.0 47 | // skipNextRefresh = false 48 | } 49 | 50 | override public var safeAreaInsets: UIEdgeInsets 51 | { 52 | .zero 53 | } 54 | 55 | func setContent(itemID: ASCollectionViewItemUniqueID, content: Content) 56 | { 57 | self.itemID = itemID 58 | hostingController.setView(AnyView(content.id(itemID))) 59 | } 60 | 61 | override func layoutSubviews() 62 | { 63 | super.layoutSubviews() 64 | 65 | hostingController.viewController.view.frame = contentView.bounds 66 | hostingController.viewController.view.layoutIfNeeded() 67 | } 68 | 69 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 70 | { 71 | let selfSizeHorizontal = selfSizingConfig.selfSizeHorizontally ?? (horizontalFittingPriority != .required) 72 | let selfSizeVertical = selfSizingConfig.selfSizeVertically ?? (verticalFittingPriority != .required) 73 | 74 | guard selfSizeVertical || selfSizeHorizontal 75 | else 76 | { 77 | return targetSize 78 | } 79 | 80 | // We need to calculate a size for self-sizing. Layout the view to get swiftUI to update its state 81 | hostingController.viewController.view.setNeedsLayout() 82 | hostingController.viewController.view.layoutIfNeeded() 83 | let size = hostingController.sizeThatFits( 84 | in: targetSize, 85 | maxSize: maxSizeForSelfSizing, 86 | selfSizeHorizontal: selfSizeHorizontal, 87 | selfSizeVertical: selfSizeVertical) 88 | return size 89 | } 90 | 91 | var maxSizeForSelfSizing: ASOptionalSize 92 | { 93 | ASOptionalSize( 94 | width: selfSizingConfig.canExceedCollectionWidth ? nil : collectionViewController.map { $0.collectionView.contentSize.width - 0.001 }, 95 | height: selfSizingConfig.canExceedCollectionHeight ? nil : collectionViewController.map { $0.collectionView.contentSize.height - 0.001 }) 96 | } 97 | 98 | var disableSwiftUIDropInteraction: Bool 99 | { 100 | get { hostingController.disableSwiftUIDropInteraction } 101 | set { hostingController.disableSwiftUIDropInteraction = newValue } 102 | } 103 | 104 | var disableSwiftUIDragInteraction: Bool 105 | { 106 | get { hostingController.disableSwiftUIDragInteraction } 107 | set { hostingController.disableSwiftUIDragInteraction = newValue } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASCollectionViewDecoration.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | public protocol Decoration: View 9 | { 10 | init() 11 | } 12 | 13 | @available(iOS 13.0, *) 14 | class ASCollectionViewDecoration: ASCollectionViewSupplementaryView 15 | { 16 | override init(frame: CGRect) 17 | { 18 | super.init(frame: frame) 19 | setContent(supplementaryID: ASSupplementaryCellID(sectionIDHash: 0, supplementaryKind: "Decoration"), content: Content()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASCollectionViewSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASCollectionViewSupplementaryView: UICollectionReusableView, ASDataSourceConfigurableSupplementary 9 | { 10 | var supplementaryID: ASSupplementaryCellID? 11 | let hostingController = ASHostingController(AnyView(EmptyView())) 12 | 13 | var isEmpty: Bool = true 14 | var selfSizingConfig: ASSelfSizingConfig = .init() 15 | 16 | override init(frame: CGRect) 17 | { 18 | super.init(frame: frame) 19 | addSubview(hostingController.viewController.view) 20 | hostingController.viewController.view.frame = bounds 21 | } 22 | 23 | @available(*, unavailable) 24 | required init?(coder: NSCoder) 25 | { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | weak var collectionViewController: AS_CollectionViewController? 30 | { 31 | didSet 32 | { 33 | if collectionViewController != oldValue 34 | { 35 | collectionViewController?.addChild(hostingController.viewController) 36 | hostingController.viewController.didMove(toParent: collectionViewController) 37 | } 38 | } 39 | } 40 | 41 | override func prepareForReuse() 42 | { 43 | super.prepareForReuse() 44 | supplementaryID = nil 45 | } 46 | 47 | func setContent(supplementaryID: ASSupplementaryCellID, content: Content?) 48 | { 49 | guard let content = content else { setAsEmpty(supplementaryID: supplementaryID); return } 50 | self.supplementaryID = supplementaryID 51 | isEmpty = false 52 | hostingController.setView(AnyView(content.id(supplementaryID))) 53 | } 54 | 55 | func setAsEmpty(supplementaryID: ASSupplementaryCellID?) 56 | { 57 | self.supplementaryID = supplementaryID 58 | isEmpty = true 59 | hostingController.setView(AnyView(EmptyView().id(supplementaryID))) 60 | } 61 | 62 | override public var safeAreaInsets: UIEdgeInsets 63 | { 64 | .zero 65 | } 66 | 67 | override func layoutSubviews() 68 | { 69 | super.layoutSubviews() 70 | 71 | hostingController.viewController.view.frame = bounds 72 | } 73 | 74 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 75 | { 76 | guard !isEmpty else { return CGSize(width: 1, height: 1) } 77 | let selfSizeHorizontal = selfSizingConfig.selfSizeHorizontally ?? (horizontalFittingPriority != .required) 78 | let selfSizeVertical = selfSizingConfig.selfSizeVertically ?? (verticalFittingPriority != .required) 79 | 80 | guard selfSizeVertical || selfSizeHorizontal 81 | else 82 | { 83 | return targetSize 84 | } 85 | 86 | // We need to calculate a size for self-sizing. Layout the view to get swiftUI to update its state 87 | hostingController.viewController.view.setNeedsLayout() 88 | hostingController.viewController.view.layoutIfNeeded() 89 | let size = hostingController.sizeThatFits( 90 | in: targetSize, 91 | maxSize: maxSizeForSelfSizing, 92 | selfSizeHorizontal: selfSizeHorizontal, 93 | selfSizeVertical: selfSizeVertical) 94 | return size 95 | } 96 | 97 | var maxSizeForSelfSizing: ASOptionalSize 98 | { 99 | ASOptionalSize( 100 | width: selfSizingConfig.canExceedCollectionWidth ? nil : collectionViewController.map { $0.collectionView.contentSize.width - 0.001 }, 101 | height: selfSizingConfig.canExceedCollectionHeight ? nil : collectionViewController.map { $0.collectionView.contentSize.height - 0.001 }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASSupplementaryCellID.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | struct ASSupplementaryCellID: Hashable 6 | { 7 | let sectionIDHash: Int 8 | let supplementaryKind: String 9 | } 10 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASTableViewCell: UITableViewCell, ASDataSourceConfigurableCell 9 | { 10 | var itemID: ASCollectionViewItemUniqueID? 11 | let hostingController = ASHostingController(AnyView(EmptyView())) 12 | // var skipNextRefresh: Bool = false 13 | 14 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) 15 | { 16 | super.init(style: .default, reuseIdentifier: reuseIdentifier) 17 | backgroundColor = nil 18 | selectionStyle = .default 19 | 20 | let selectedBack = UIView() 21 | selectedBack.backgroundColor = UIColor.systemGray.withAlphaComponent(0.1) 22 | selectedBackgroundView = selectedBack 23 | 24 | contentView.addSubview(hostingController.viewController.view) 25 | hostingController.viewController.view.frame = contentView.bounds 26 | } 27 | 28 | @available(*, unavailable) 29 | required init?(coder: NSCoder) 30 | { 31 | fatalError("init(coder:) has not been implemented") 32 | } 33 | 34 | weak var tableViewController: AS_TableViewController? 35 | { 36 | didSet 37 | { 38 | if tableViewController != oldValue 39 | { 40 | hostingController.viewController.didMove(toParent: tableViewController) 41 | tableViewController?.addChild(hostingController.viewController) 42 | } 43 | } 44 | } 45 | 46 | override func prepareForReuse() 47 | { 48 | super.prepareForReuse() 49 | 50 | itemID = nil 51 | isSelected = false 52 | backgroundColor = nil 53 | alpha = 1.0 54 | // skipNextRefresh = false 55 | } 56 | 57 | func setContent(itemID: ASCollectionViewItemUniqueID, content: Content) 58 | { 59 | self.itemID = itemID 60 | hostingController.setView(AnyView(content.id(itemID))) 61 | } 62 | 63 | override public var safeAreaInsets: UIEdgeInsets 64 | { 65 | .zero 66 | } 67 | 68 | override func layoutSubviews() 69 | { 70 | super.layoutSubviews() 71 | 72 | hostingController.viewController.view.frame = contentView.bounds 73 | } 74 | 75 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 76 | { 77 | hostingController.viewController.view.setNeedsLayout() 78 | hostingController.viewController.view.layoutIfNeeded() 79 | let size = hostingController.sizeThatFits( 80 | in: targetSize, 81 | maxSize: ASOptionalSize(), 82 | selfSizeHorizontal: false, 83 | selfSizeVertical: true) 84 | return size 85 | } 86 | 87 | var disableSwiftUIDropInteraction: Bool 88 | { 89 | get { hostingController.disableSwiftUIDropInteraction } 90 | set { hostingController.disableSwiftUIDropInteraction = newValue } 91 | } 92 | 93 | var disableSwiftUIDragInteraction: Bool 94 | { 95 | get { hostingController.disableSwiftUIDragInteraction } 96 | set { hostingController.disableSwiftUIDragInteraction = newValue } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Cells/ASTableViewSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASTableViewSupplementaryView: UITableViewHeaderFooterView, ASDataSourceConfigurableSupplementary 9 | { 10 | var supplementaryID: ASSupplementaryCellID? 11 | let hostingController = ASHostingController(AnyView(EmptyView())) 12 | var isEmpty: Bool = true 13 | 14 | override init(reuseIdentifier: String?) 15 | { 16 | super.init(reuseIdentifier: reuseIdentifier) 17 | backgroundView = UIView() 18 | contentView.addSubview(hostingController.viewController.view) 19 | hostingController.viewController.view.frame = contentView.bounds 20 | } 21 | 22 | @available(*, unavailable) 23 | required init?(coder: NSCoder) 24 | { 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | 28 | weak var tableViewController: AS_TableViewController? 29 | { 30 | didSet 31 | { 32 | if tableViewController != oldValue 33 | { 34 | hostingController.viewController.didMove(toParent: tableViewController) 35 | tableViewController?.addChild(hostingController.viewController) 36 | } 37 | } 38 | } 39 | 40 | override func prepareForReuse() 41 | { 42 | super.prepareForReuse() 43 | supplementaryID = nil 44 | } 45 | 46 | func setContent(supplementaryID: ASSupplementaryCellID, content: Content?) 47 | { 48 | guard let content = content else { setAsEmpty(supplementaryID: supplementaryID); return } 49 | self.supplementaryID = supplementaryID 50 | isEmpty = false 51 | hostingController.setView(AnyView(content.id(supplementaryID))) 52 | } 53 | 54 | func setAsEmpty(supplementaryID: ASSupplementaryCellID?) 55 | { 56 | self.supplementaryID = supplementaryID 57 | isEmpty = true 58 | hostingController.setView(AnyView(EmptyView().id(supplementaryID))) 59 | } 60 | 61 | override public var safeAreaInsets: UIEdgeInsets 62 | { 63 | .zero 64 | } 65 | 66 | override func layoutSubviews() 67 | { 68 | super.layoutSubviews() 69 | 70 | hostingController.viewController.view.frame = contentView.bounds 71 | } 72 | 73 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize 74 | { 75 | guard !isEmpty else { return CGSize(width: 1, height: 1) } 76 | hostingController.viewController.view.setNeedsLayout() 77 | hostingController.viewController.view.layoutIfNeeded() 78 | let size = hostingController.sizeThatFits( 79 | in: targetSize, 80 | maxSize: ASOptionalSize(), 81 | selfSizeHorizontal: false, 82 | selfSizeVertical: true) 83 | return size 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Config/ASDragDropConfig.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | public struct ASDragDropConfig 9 | { 10 | // MARK: Automatic handling 11 | 12 | var dataBinding: Binding<[Data]>? 13 | 14 | // MARK: Manual handling 15 | 16 | var onDeleteOrRemoveItems: ((_ indexSet: IndexSet) -> Bool)? 17 | var onInsertItems: ((_ index: Int, _ items: [Data]) -> Bool)? 18 | var onMoveItem: ((_ from: Int, _ to: Int) -> Bool)? 19 | 20 | // MARK: Shared 21 | 22 | var dragEnabled: Bool = false 23 | var dropEnabled: Bool = false 24 | var reorderingEnabled: Bool = false 25 | 26 | /// Called to check whether an item can be dragged 27 | var canDragItem: ((_ indexPath: IndexPath) -> Bool)? 28 | 29 | /// Called to check whether an item can be moved to the specified indexPath 30 | var canMoveItem: ((_ sourceIndexPath: IndexPath, _ destinationIndexPath: IndexPath) -> Bool)? 31 | 32 | /// Called to check whether an item can be dropped 33 | var canDropItem: ((_ indexPath: IndexPath) -> Bool)? 34 | 35 | var dragItemProvider: ((_ item: Data) -> NSItemProvider?)? 36 | 37 | var dropItemProvider: ((_ sourceItem: Data?, _ dragItem: UIDragItem) -> Data?)? 38 | 39 | init() 40 | { 41 | // Used to provide `disabled` mode 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Config/ClosureTypeAliases.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import UIKit 5 | 6 | @available(iOS 13.0, *) 7 | public enum CellEvent 8 | { 9 | /// Respond by starting necessary prefetch operations for this data to be displayed soon (eg. download images) 10 | case prefetchForData(data: [Data]) 11 | 12 | /// Called when its no longer necessary to prefetch this data 13 | case cancelPrefetchForData(data: [Data]) 14 | 15 | /// Called when an item is appearing on the screen 16 | case onAppear(item: Data) 17 | 18 | /// Called when an item is disappearing from the screen 19 | case onDisappear(item: Data) 20 | } 21 | 22 | @available(iOS 13.0, *) 23 | public typealias OnCellEvent = ((_ event: CellEvent) -> Void) 24 | 25 | @available(iOS 13.0, *) 26 | public typealias ShouldAllowSwipeToDelete = ((_ index: Int) -> Bool) 27 | 28 | @available(iOS 13.0, *) 29 | public typealias OnSwipeToDelete = ((_ index: Int, _ item: Data) -> Bool) 30 | 31 | @available(iOS 13.0, *) 32 | public typealias ContextMenuProvider = ((_ index: Int, _ item: Data) -> UIContextMenuConfiguration?) 33 | 34 | @available(iOS 13.0, *) 35 | public typealias SelfSizingConfig = ((_ context: ASSelfSizingContext) -> ASSelfSizingConfig?) 36 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Datasource/ASDiffableDataSource.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import DifferenceKit 4 | import Foundation 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASDiffableDataSource: NSObject 9 | { 10 | public internal(set) var currentSnapshot = ASDiffableDataSourceSnapshot() 11 | 12 | func identifier(at indexPath: IndexPath) -> ASCollectionViewItemUniqueID 13 | { 14 | currentSnapshot.sections[indexPath.section].elements[indexPath.item].differenceIdentifier 15 | } 16 | } 17 | 18 | @available(iOS 13.0, *) 19 | struct ASDiffableDataSourceSnapshot 20 | { 21 | private(set) var sections: [Section] 22 | private(set) var itemPositionMap: [ASCollectionViewItemUniqueID: ItemPosition] = [:] 23 | 24 | init(sections: [Section] = []) 25 | { 26 | self.sections = sections 27 | sections.enumerated().forEach 28 | { sectionIndex, section in 29 | section.elements.enumerated().forEach { itemIndex, item in itemPositionMap[item.differenceIdentifier] = ItemPosition(itemIndex: itemIndex, sectionIndex: sectionIndex) } 30 | } 31 | } 32 | 33 | mutating func appendSection(sectionID: SectionID, items: [ASCollectionViewItemUniqueID]) 34 | { 35 | let newSection = Section(id: sectionID, elements: items) 36 | sections.append(newSection) 37 | newSection.elements.enumerated().forEach { itemIndex, item in itemPositionMap[item.differenceIdentifier] = ItemPosition(itemIndex: itemIndex, sectionIndex: sections.endIndex - 1) } 38 | } 39 | 40 | mutating func removeItems(fromSectionIndex sectionIndex: Int, atOffsets offsets: IndexSet) 41 | { 42 | guard sections.containsIndex(sectionIndex) else { return } 43 | sections[sectionIndex].elements.remove(atOffsets: offsets) 44 | } 45 | 46 | mutating func insertItems(_ items: [ASCollectionViewItemUniqueID], atSectionIndex sectionIndex: Int, atOffset offset: Int) 47 | { 48 | guard sections.containsIndex(sectionIndex) else { return } 49 | sections[sectionIndex].elements.insert(contentsOf: items.map { Item(id: $0) }, at: offset) 50 | } 51 | 52 | mutating func reloadItems(items: Set) 53 | { 54 | items.forEach 55 | { item in 56 | guard let position = itemPositionMap[item] else { return } 57 | sections[position.sectionIndex].elements[position.itemIndex].shouldReload = true 58 | } 59 | } 60 | 61 | mutating func moveItem(fromIndexPath: IndexPath, toIndexPath: IndexPath) 62 | { 63 | guard sections.containsIndex(fromIndexPath.section), sections.containsIndex(toIndexPath.section) else { return } 64 | if fromIndexPath.section == toIndexPath.section 65 | { 66 | let item = sections[fromIndexPath.section].elements.remove(at: fromIndexPath.item) 67 | sections[toIndexPath.section].elements.insert(item, at: toIndexPath.item) 68 | } 69 | else 70 | { 71 | let item = sections[fromIndexPath.section].elements.remove(at: fromIndexPath.item) 72 | sections[toIndexPath.section].elements.insert(item, at: toIndexPath.item) 73 | } 74 | } 75 | 76 | struct ItemPosition 77 | { 78 | var itemIndex: Int 79 | var sectionIndex: Int 80 | } 81 | 82 | struct Section 83 | { 84 | var id: SectionID 85 | var elements: [Item] 86 | 87 | var differenceIdentifier: SectionID 88 | { 89 | id 90 | } 91 | 92 | func isContentEqual(to source: ASDiffableDataSourceSnapshot.Section) -> Bool 93 | { 94 | source.differenceIdentifier == differenceIdentifier 95 | } 96 | } 97 | 98 | struct Item: Differentiable 99 | { 100 | var differenceIdentifier: ASCollectionViewItemUniqueID 101 | var shouldReload: Bool 102 | 103 | init(id: ASCollectionViewItemUniqueID, shouldReload: Bool = false) 104 | { 105 | differenceIdentifier = id 106 | self.shouldReload = shouldReload 107 | } 108 | 109 | func isContentEqual(to source: Item) -> Bool 110 | { 111 | !shouldReload && differenceIdentifier == source.differenceIdentifier 112 | } 113 | } 114 | } 115 | 116 | @available(iOS 13.0, *) 117 | extension ASDiffableDataSourceSnapshot.Section 118 | { 119 | init(id: SectionID, elements: [ASCollectionViewItemUniqueID], shouldReloadElements: Bool = false) 120 | { 121 | self.id = id 122 | self.elements = elements.map { ASDiffableDataSourceSnapshot.Item(id: $0, shouldReload: shouldReloadElements) } 123 | } 124 | } 125 | 126 | @available(iOS 13.0, *) 127 | extension ASDiffableDataSourceSnapshot.Section: DifferentiableSection 128 | { 129 | init(source: Self, elements: C) where C.Element == ASDiffableDataSourceSnapshot.Item 130 | { 131 | self.init(id: source.differenceIdentifier, elements: Array(elements)) 132 | } 133 | } 134 | 135 | @available(iOS 13.0, *) 136 | extension ASCollectionViewItemUniqueID: Differentiable {} 137 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Datasource/ASDiffableDataSourceCollectionView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import DifferenceKit 4 | import SwiftUI 5 | import UIKit 6 | 7 | @available(iOS 13.0, *) 8 | class ASDiffableDataSourceCollectionView: ASDiffableDataSource, UICollectionViewDataSource 9 | { 10 | /// The type of closure providing the cell. 11 | public typealias Snapshot = ASDiffableDataSourceSnapshot 12 | public typealias CellProvider = (UICollectionView, IndexPath, ASCollectionViewItemUniqueID) -> ASCollectionViewCell? 13 | public typealias SupplementaryProvider = (UICollectionView, String, IndexPath) -> ASCollectionViewSupplementaryView 14 | 15 | private weak var collectionView: UICollectionView? 16 | var cellProvider: CellProvider 17 | var supplementaryViewProvider: SupplementaryProvider 18 | 19 | public init(collectionView: UICollectionView, cellProvider: @escaping CellProvider, supplementaryViewProvider: @escaping SupplementaryProvider) 20 | { 21 | self.collectionView = collectionView 22 | self.cellProvider = cellProvider 23 | self.supplementaryViewProvider = supplementaryViewProvider 24 | super.init() 25 | 26 | collectionView.dataSource = self 27 | } 28 | 29 | private var firstLoad: Bool = true 30 | 31 | func applySnapshot(_ newSnapshot: Snapshot, animated: Bool = true, completion: (() -> Void)? = nil) 32 | { 33 | let changeset = StagedChangeset(source: currentSnapshot.sections, target: newSnapshot.sections) 34 | 35 | guard let collectionView = collectionView else { return } 36 | 37 | let apply = { 38 | collectionView.reload(using: changeset, interrupt: { $0.changeCount > 100 }) 39 | { newSections in 40 | self.currentSnapshot = .init(sections: newSections) 41 | } 42 | } 43 | if firstLoad || !animated 44 | { 45 | UIView.performWithoutAnimation(apply) 46 | } 47 | else 48 | { 49 | apply() 50 | } 51 | completion?() 52 | firstLoad = false 53 | } 54 | 55 | func numberOfSections(in collectionView: UICollectionView) -> Int 56 | { 57 | currentSnapshot.sections.count 58 | } 59 | 60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 61 | { 62 | currentSnapshot.sections[section].elements.count 63 | } 64 | 65 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 66 | { 67 | let itemIdentifier = identifier(at: indexPath) 68 | guard let cell = cellProvider(collectionView, indexPath, itemIdentifier) 69 | else 70 | { 71 | fatalError("ASCollectionView dataSource returned a nil cell for row at index path: \(indexPath), collectionView: \(collectionView), itemIdentifier: \(itemIdentifier)") 72 | } 73 | return cell 74 | } 75 | 76 | func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView 77 | { 78 | return supplementaryViewProvider(collectionView, kind, indexPath) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Datasource/ASDiffableDataSourceTableView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import DifferenceKit 4 | import UIKit 5 | 6 | @available(iOS 13.0, *) 7 | class ASDiffableDataSourceTableView: ASDiffableDataSource, UITableViewDataSource 8 | { 9 | /// The type of closure providing the cell. 10 | public typealias Snapshot = ASDiffableDataSourceSnapshot 11 | public typealias CellProvider = (UITableView, IndexPath, ASCollectionViewItemUniqueID) -> ASTableViewCell? 12 | 13 | private weak var tableView: UITableView? 14 | private let cellProvider: CellProvider 15 | private var indexTitles: [(Int, String)] = [] 16 | 17 | var canSelect: ((_ indexPath: IndexPath) -> Bool)? 18 | var canDelete: ((_ indexPath: IndexPath) -> Bool)? 19 | var onDelete: ((_ indexPath: IndexPath) -> Bool)? 20 | var canMove: ((_ source: IndexPath) -> Bool)? 21 | var onMove: ((_ source: IndexPath, _ destination: IndexPath) -> Bool)? 22 | 23 | public init(tableView: UITableView, cellProvider: @escaping CellProvider) 24 | { 25 | self.tableView = tableView 26 | self.cellProvider = cellProvider 27 | super.init() 28 | 29 | tableView.dataSource = self 30 | } 31 | 32 | /// The default animation to updating the views. 33 | public var defaultRowAnimation: UITableView.RowAnimation = .automatic 34 | 35 | private var firstLoad: Bool = true 36 | private var canRefreshSizes: Bool = false 37 | 38 | func setIndexTitles(_ titles: [(Int, String)]) 39 | { 40 | var strings = Set() 41 | let uniqued = titles.filter 42 | { (index, string) -> Bool in 43 | guard !strings.contains(string) else { return false } 44 | strings.insert(string) 45 | return true 46 | } 47 | indexTitles = uniqued 48 | } 49 | 50 | func applySnapshot(_ newSnapshot: Snapshot, animated: Bool = true, completion: (() -> Void)? = nil) 51 | { 52 | guard let tableView = tableView else { return } 53 | 54 | firstLoad = false 55 | 56 | let changeset = StagedChangeset(source: currentSnapshot.sections, target: newSnapshot.sections) 57 | let shouldDisableAnimation = firstLoad || !animated 58 | 59 | canRefreshSizes = false 60 | CATransaction.begin() 61 | if shouldDisableAnimation 62 | { 63 | CATransaction.setDisableActions(true) 64 | } 65 | CATransaction.setCompletionBlock 66 | { [weak self] in 67 | self?.canRefreshSizes = true 68 | completion?() 69 | } 70 | tableView.reload(using: changeset, with: shouldDisableAnimation ? .none : .automatic) 71 | { newSections in 72 | self.currentSnapshot = .init(sections: newSections) 73 | } 74 | CATransaction.commit() 75 | } 76 | 77 | func updateCellSizes(animated: Bool = true) 78 | { 79 | guard let tableView = tableView, canRefreshSizes, !tableView.visibleCells.isEmpty else { return } 80 | CATransaction.begin() 81 | if !animated 82 | { 83 | CATransaction.setDisableActions(true) 84 | } 85 | tableView.performBatchUpdates(nil, completion: nil) 86 | 87 | CATransaction.commit() 88 | } 89 | 90 | func didDisappear() 91 | { 92 | canRefreshSizes = false 93 | } 94 | 95 | func numberOfSections(in tableView: UITableView) -> Int 96 | { 97 | currentSnapshot.sections.count 98 | } 99 | 100 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int 101 | { 102 | currentSnapshot.sections[section].elements.count 103 | } 104 | 105 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 106 | { 107 | let itemIdentifier = identifier(at: indexPath) 108 | guard let cell = cellProvider(tableView, indexPath, itemIdentifier) 109 | else 110 | { 111 | fatalError("ASTableView dataSource returned a nil cell for row at index path: \(indexPath), tableView: \(tableView), itemIdentifier: \(itemIdentifier)") 112 | } 113 | return cell 114 | } 115 | 116 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool 117 | { 118 | canSelect?(indexPath) ?? false || canMove?(indexPath) ?? false || canDelete?(indexPath) ?? false 119 | } 120 | 121 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) 122 | { 123 | guard let onDelete = onDelete else { return } 124 | let didDelete = onDelete(indexPath) 125 | if didDelete 126 | { 127 | var deleteSnapshot = currentSnapshot 128 | deleteSnapshot.removeItems(fromSectionIndex: indexPath.section, atOffsets: [indexPath.row]) 129 | applySnapshot(deleteSnapshot, animated: true, completion: nil) 130 | } 131 | } 132 | 133 | func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool 134 | { 135 | canMove?(indexPath) ?? (onMove != nil) 136 | } 137 | 138 | func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 139 | { 140 | guard let onMove = onMove else { return } 141 | let didMove = onMove(sourceIndexPath, destinationIndexPath) 142 | if didMove 143 | { 144 | var moveSnapshot = currentSnapshot 145 | moveSnapshot.moveItem(fromIndexPath: sourceIndexPath, toIndexPath: destinationIndexPath) 146 | applySnapshot(moveSnapshot, animated: true, completion: nil) 147 | } 148 | } 149 | 150 | // MARK: Index titles support 151 | 152 | func sectionIndexTitles(for tableView: UITableView) -> [String]? 153 | { 154 | indexTitles.isEmpty ? nil : indexTitles.map(\.1) 155 | } 156 | 157 | func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int 158 | { 159 | guard let index = indexTitles[safe: index]?.0, currentSnapshot.sections.indices.contains(index) else { return 0 } 160 | return index 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Delegate/ASCollectionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | /// ASCollectionViewDelegate: Subclass this to create a custom delegate (eg. for supporting UICollectionViewLayouts that default to using the collectionView delegate) 7 | @available(iOS 13.0, *) 8 | open class ASCollectionViewDelegate: NSObject, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout 9 | { 10 | weak var coordinator: ASCollectionViewCoordinator? 11 | 12 | public func getDataForItem(at indexPath: IndexPath) -> Any? 13 | { 14 | coordinator?.typeErasedDataForItem(at: indexPath) 15 | } 16 | 17 | public func getDataForItem(at indexPath: IndexPath) -> T? 18 | { 19 | coordinator?.typeErasedDataForItem(at: indexPath) as? T 20 | } 21 | 22 | open func collectionViewSelfSizingSettings(forContext: ASSelfSizingContext) -> ASSelfSizingConfig? 23 | { 24 | nil 25 | } 26 | 27 | open var collectionViewContentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior 28 | { 29 | .automatic 30 | } 31 | 32 | open func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) 33 | { 34 | coordinator?.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath) 35 | } 36 | 37 | open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) 38 | { 39 | coordinator?.collectionView(collectionView, didEndDisplaying: cell, forItemAt: indexPath) 40 | } 41 | 42 | open func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) 43 | { 44 | coordinator?.collectionView(collectionView, willDisplaySupplementaryView: view, forElementKind: elementKind, at: indexPath) 45 | } 46 | 47 | open func collectionView(_ collectionView: UICollectionView, didEndDisplayingSupplementaryView view: UICollectionReusableView, forElementOfKind elementKind: String, at indexPath: IndexPath) 48 | { 49 | coordinator?.collectionView(collectionView, didEndDisplayingSupplementaryView: view, forElementOfKind: elementKind, at: indexPath) 50 | } 51 | 52 | open func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool 53 | { 54 | coordinator?.collectionView(collectionView, shouldHighlightItemAt: indexPath) ?? true 55 | } 56 | 57 | open func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) 58 | { 59 | coordinator?.collectionView(collectionView, didHighlightItemAt: indexPath) 60 | } 61 | 62 | open func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) 63 | { 64 | coordinator?.collectionView(collectionView, didUnhighlightItemAt: indexPath) 65 | } 66 | 67 | open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool 68 | { 69 | coordinator?.collectionView(collectionView, shouldSelectItemAt: indexPath) ?? true 70 | } 71 | 72 | open func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool 73 | { 74 | coordinator?.collectionView(collectionView, shouldDeselectItemAt: indexPath) ?? true 75 | } 76 | 77 | open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) 78 | { 79 | coordinator?.collectionView(collectionView, didSelectItemAt: indexPath) 80 | } 81 | 82 | open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) 83 | { 84 | coordinator?.collectionView(collectionView, didDeselectItemAt: indexPath) 85 | } 86 | 87 | /* 88 | //REPLACED WITH CUSTOM PREFETCH SOLUTION AS PREFETCH API WAS NOT WORKING FOR COMPOSITIONAL LAYOUT 89 | public func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) 90 | public func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) 91 | */ 92 | } 93 | 94 | @available(iOS 13.0, *) 95 | extension ASCollectionViewDelegate: UICollectionViewDragDelegate, UICollectionViewDropDelegate 96 | { 97 | open func collectionView(_ collectionView: UICollectionView, dragSessionAllowsMoveOperation session: UIDragSession) -> Bool 98 | { 99 | true 100 | } 101 | 102 | open func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] 103 | { 104 | coordinator?.collectionView(collectionView, itemsForBeginning: session, at: indexPath) ?? [] 105 | } 106 | 107 | open func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal 108 | { 109 | coordinator?.collectionView(collectionView, dropSessionDidUpdate: session, withDestinationIndexPath: destinationIndexPath) ?? UICollectionViewDropProposal(operation: .cancel) 110 | } 111 | 112 | // UICollectionView doesn't support dropping multiple items :( [http://www.openradar.me/42068699] 113 | open func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) 114 | { 115 | self.coordinator?.collectionView(collectionView, performDropWith: coordinator) 116 | } 117 | 118 | open func scrollViewDidScroll(_ scrollView: UIScrollView) 119 | { 120 | coordinator?.scrollViewDidScroll(scrollView) 121 | } 122 | } 123 | 124 | @available(iOS 13.0, *) 125 | extension ASCollectionViewDelegate 126 | { 127 | open func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? 128 | { 129 | coordinator?.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPath, point: point) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Environment/EnvironmentKeys.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | // MARK: Internal Key Definitions 7 | 8 | @available(iOS 13.0, *) 9 | struct EnvironmentKeyInvalidateCellLayout: EnvironmentKey 10 | { 11 | static let defaultValue: ((_ animated: Bool) -> Void)? = nil 12 | } 13 | 14 | struct EnvironmentKeyTableViewScrollToCell: EnvironmentKey 15 | { 16 | static let defaultValue: ((UITableView.ScrollPosition) -> Void)? = nil 17 | } 18 | 19 | struct EnvironmentKeyCollectionViewScrollToCell: EnvironmentKey 20 | { 21 | static let defaultValue: ((UICollectionView.ScrollPosition) -> Void)? = nil 22 | } 23 | 24 | // MARK: Internal Helpers 25 | 26 | @available(iOS 13.0, *) 27 | public extension EnvironmentValues 28 | { 29 | var invalidateCellLayout: ((_ animated: Bool) -> Void)? 30 | { 31 | get { self[EnvironmentKeyInvalidateCellLayout.self] } 32 | set { self[EnvironmentKeyInvalidateCellLayout.self] = newValue } 33 | } 34 | 35 | var tableViewScrollToCell: ((UITableView.ScrollPosition) -> Void)? 36 | { 37 | get { self[EnvironmentKeyTableViewScrollToCell.self] } 38 | set { self[EnvironmentKeyTableViewScrollToCell.self] = newValue } 39 | } 40 | 41 | var collectionViewScrollToCell: ((UICollectionView.ScrollPosition) -> Void)? 42 | { 43 | get { self[EnvironmentKeyCollectionViewScrollToCell.self] } 44 | set { self[EnvironmentKeyCollectionViewScrollToCell.self] = newValue } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/FunctionBuilders/SectionArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public protocol Nestable 8 | { 9 | associatedtype T 10 | func asArray() -> [T] 11 | } 12 | 13 | @available(iOS 13.0, *) 14 | extension ASSection: Nestable 15 | { 16 | public func asArray() -> [ASSection] 17 | { 18 | [self] 19 | } 20 | } 21 | 22 | @available(iOS 13.0, *) 23 | extension Optional: Nestable where Wrapped: Nestable 24 | { 25 | public func asArray() -> [Wrapped.T] 26 | { 27 | map { $0.asArray() } ?? [] 28 | } 29 | } 30 | 31 | @available(iOS 13.0, *) 32 | extension Array: Nestable 33 | { 34 | public func asArray() -> Self 35 | { 36 | self 37 | } 38 | } 39 | 40 | @available(iOS 13.0, *) 41 | public func buildSectionArray(@SectionArrayBuilder _ sections: () -> [ASSection]) -> [ASSection] 42 | { 43 | sections() 44 | } 45 | 46 | @available(iOS 13.0, *) 47 | @resultBuilder 48 | public struct SectionArrayBuilder where SectionID: Hashable 49 | { 50 | public typealias Section = ASCollectionViewSection 51 | public typealias Output = [Section] 52 | 53 | public static func buildExpression(_ section: ASSection?) -> Output 54 | { 55 | section?.asArray() ?? [] 56 | } 57 | 58 | public static func buildExpression(_ sections: [ASSection]) -> Output 59 | { 60 | sections 61 | } 62 | 63 | public static func buildEither(first: Output) -> Output 64 | { 65 | first.asArray() 66 | } 67 | 68 | public static func buildEither(second: Output) -> Output 69 | { 70 | second.asArray() 71 | } 72 | 73 | public static func buildIf(_ item: Output?) -> Output 74 | { 75 | item?.asArray() ?? [] 76 | } 77 | 78 | public static func buildBlock(_ item0: Output) -> Output 79 | { 80 | item0.asArray() 81 | } 82 | 83 | public static func buildBlock(_ items: Output...) -> Output 84 | { 85 | items.flatMap { $0 } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/FunctionBuilders/ViewArrayBuilder.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | @resultBuilder 8 | public struct ViewArrayBuilder 9 | { 10 | public enum Wrapper 11 | { 12 | case empty 13 | case view(AnyView) 14 | case group([Wrapper]) 15 | 16 | init(_ view: Content) 17 | { 18 | self = .view(AnyView(view)) 19 | } 20 | 21 | func flattened() -> [AnyView] 22 | { 23 | switch self 24 | { 25 | case .empty: 26 | return [] 27 | case let .view(theView): 28 | return [theView] 29 | case let .group(wrappers): 30 | return wrappers.flatMap { $0.flattened() } 31 | } 32 | } 33 | } 34 | 35 | public typealias Output = Wrapper 36 | 37 | public static func buildExpression(_ view: Content?) -> Wrapper 38 | { 39 | view.map { Wrapper($0) } ?? .empty 40 | } 41 | 42 | public static func buildExpression(_ views: [Content]?) -> Wrapper 43 | { 44 | guard let views = views else { return .empty } 45 | return Wrapper.group(views.map { Wrapper($0) }) 46 | } 47 | 48 | public static func buildEither(first: Wrapper) -> Output 49 | { 50 | first 51 | } 52 | 53 | public static func buildEither(second: Wrapper) -> Output 54 | { 55 | second 56 | } 57 | 58 | public static func buildIf(_ item: Wrapper?) -> Output 59 | { 60 | item ?? .empty 61 | } 62 | 63 | public static func buildBlock(_ item0: Wrapper) -> Output 64 | { 65 | item0 66 | } 67 | 68 | public static func buildBlock(_ items: Wrapper...) -> Output 69 | { 70 | .group(items) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Implementation/ASHostingController.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | internal struct ASHostingControllerWrapper: View, ASHostingControllerWrapperProtocol 8 | { 9 | var invalidateCellLayoutCallback: ((_ animated: Bool) -> Void)? 10 | var collectionViewScrollToCellCallback: ((UICollectionView.ScrollPosition) -> Void)? 11 | var tableViewScrollToCellCallback: ((UITableView.ScrollPosition) -> Void)? 12 | 13 | var content: Content 14 | var body: some View 15 | { 16 | content 17 | .environment(\.invalidateCellLayout, invalidateCellLayoutCallback) 18 | .environment(\.collectionViewScrollToCell, collectionViewScrollToCellCallback) 19 | .environment(\.tableViewScrollToCell, tableViewScrollToCellCallback) 20 | } 21 | } 22 | 23 | protocol ASHostingControllerWrapperProtocol 24 | { 25 | var invalidateCellLayoutCallback: ((_ animated: Bool) -> Void)? { get set } 26 | var collectionViewScrollToCellCallback: ((UICollectionView.ScrollPosition) -> Void)? { get set } 27 | var tableViewScrollToCellCallback: ((UITableView.ScrollPosition) -> Void)? { get set } 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | internal protocol ASHostingControllerProtocol: AnyObject, ASHostingControllerWrapperProtocol 32 | { 33 | var viewController: UIViewController { get } 34 | func sizeThatFits(in size: CGSize, maxSize: ASOptionalSize, selfSizeHorizontal: Bool, selfSizeVertical: Bool) -> CGSize 35 | } 36 | 37 | @available(iOS 13.0, *) 38 | internal class ASHostingController: ASHostingControllerProtocol 39 | { 40 | init(_ view: ViewType) 41 | { 42 | uiHostingController = .init(rootView: ASHostingControllerWrapper(content: view)) 43 | } 44 | 45 | private let uiHostingController: AS_UIHostingController> 46 | var viewController: UIViewController 47 | { 48 | uiHostingController.view.backgroundColor = .clear 49 | uiHostingController.view.insetsLayoutMarginsFromSafeArea = false 50 | return uiHostingController as UIViewController 51 | } 52 | 53 | var disableSwiftUIDropInteraction: Bool 54 | { 55 | get { uiHostingController.shouldDisableDrop } 56 | set { uiHostingController.shouldDisableDrop = newValue } 57 | } 58 | 59 | var disableSwiftUIDragInteraction: Bool 60 | { 61 | get { uiHostingController.shouldDisableDrag } 62 | set { uiHostingController.shouldDisableDrag = newValue } 63 | } 64 | 65 | var hostedView: ViewType 66 | { 67 | get 68 | { 69 | uiHostingController.rootView.content 70 | } 71 | set 72 | { 73 | uiHostingController.rootView.content = newValue 74 | } 75 | } 76 | 77 | var invalidateCellLayoutCallback: ((_ animated: Bool) -> Void)? 78 | { 79 | get 80 | { 81 | uiHostingController.rootView.invalidateCellLayoutCallback 82 | } 83 | set 84 | { 85 | uiHostingController.rootView.invalidateCellLayoutCallback = newValue 86 | } 87 | } 88 | 89 | var collectionViewScrollToCellCallback: ((UICollectionView.ScrollPosition) -> Void)? 90 | { 91 | get 92 | { 93 | uiHostingController.rootView.collectionViewScrollToCellCallback 94 | } 95 | set 96 | { 97 | uiHostingController.rootView.collectionViewScrollToCellCallback = newValue 98 | } 99 | } 100 | 101 | var tableViewScrollToCellCallback: ((UITableView.ScrollPosition) -> Void)? 102 | { 103 | get 104 | { 105 | uiHostingController.rootView.tableViewScrollToCellCallback 106 | } 107 | set 108 | { 109 | uiHostingController.rootView.tableViewScrollToCellCallback = newValue 110 | } 111 | } 112 | 113 | func setView(_ view: ViewType) 114 | { 115 | hostedView = view 116 | } 117 | 118 | func sizeThatFits(in size: CGSize, maxSize: ASOptionalSize, selfSizeHorizontal: Bool, selfSizeVertical: Bool) -> CGSize 119 | { 120 | guard selfSizeHorizontal || selfSizeVertical 121 | else 122 | { 123 | return size.applyMaxSize(maxSize) 124 | } 125 | viewController.view.layoutIfNeeded() 126 | let fittingSize = CGSize( 127 | width: selfSizeHorizontal ? maxSize.width ?? .greatestFiniteMagnitude : size.width.applyOptionalMaxBound(maxSize.width), 128 | height: selfSizeVertical ? maxSize.height ?? .greatestFiniteMagnitude : size.height.applyOptionalMaxBound(maxSize.height)) 129 | 130 | // Find the desired size 131 | var desiredSize = uiHostingController.sizeThatFits(in: fittingSize) 132 | 133 | // Accounting for 'greedy' swiftUI views that take up as much space as they can 134 | switch (desiredSize.width, desiredSize.height) 135 | { 136 | case (.greatestFiniteMagnitude, .greatestFiniteMagnitude): 137 | desiredSize = uiHostingController.sizeThatFits(in: size.applyMaxSize(maxSize)) 138 | case (.greatestFiniteMagnitude, _): 139 | desiredSize = uiHostingController.sizeThatFits(in: CGSize( 140 | width: size.width.applyOptionalMaxBound(maxSize.width), 141 | height: fittingSize.height)) 142 | case (_, .greatestFiniteMagnitude): 143 | desiredSize = uiHostingController.sizeThatFits(in: CGSize( 144 | width: fittingSize.width, 145 | height: size.height.applyOptionalMaxBound(maxSize.height))) 146 | default: break 147 | } 148 | 149 | // Ensure correct dimensions in non-self sizing axes 150 | if !selfSizeHorizontal { desiredSize.width = size.width } 151 | if !selfSizeVertical { desiredSize.height = size.height } 152 | 153 | return desiredSize.applyMaxSize(maxSize) 154 | } 155 | } 156 | 157 | @available(iOS 13.0, *) 158 | private class AS_UIHostingController: UIHostingController 159 | { 160 | var shouldDisableDrop: Bool = false 161 | { 162 | didSet 163 | { 164 | if shouldDisableDrop != oldValue 165 | { 166 | disableInteractionsIfNeeded() 167 | } 168 | } 169 | } 170 | 171 | var shouldDisableDrag: Bool = false 172 | { 173 | didSet 174 | { 175 | if shouldDisableDrag != oldValue 176 | { 177 | disableInteractionsIfNeeded() 178 | } 179 | } 180 | } 181 | 182 | func disableInteractionsIfNeeded() 183 | { 184 | guard let view = viewIfLoaded else { return } 185 | if shouldDisableDrop 186 | { 187 | if let dropInteraction = view.interactions.first(where: { 188 | $0.isKind(of: UIDropInteraction.self) 189 | }) as? UIDropInteraction 190 | { 191 | view.removeInteraction(dropInteraction) 192 | } 193 | } 194 | if shouldDisableDrag 195 | { 196 | if let contextInteraction = view.interactions.first(where: { 197 | $0.isKind(of: UIDragInteraction.self) 198 | }) as? UIDragInteraction 199 | { 200 | view.removeInteraction(contextInteraction) 201 | } 202 | } 203 | } 204 | 205 | func disableSafeArea() 206 | { 207 | guard let viewClass = object_getClass(view) else { return } 208 | 209 | let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") 210 | if let viewSubclass = NSClassFromString(viewSubclassName) 211 | { 212 | object_setClass(view, viewSubclass) 213 | } 214 | else 215 | { 216 | guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } 217 | guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } 218 | 219 | if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) 220 | { 221 | let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in 222 | .zero 223 | } 224 | class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) 225 | } 226 | 227 | if let method2 = class_getInstanceMethod(viewClass, NSSelectorFromString("keyboardWillShowWithNotification:")) 228 | { 229 | let keyboardWillShow: @convention(block) (AnyObject, AnyObject) -> Void = { _, _ in } 230 | class_addMethod(viewSubclass, NSSelectorFromString("keyboardWillShowWithNotification:"), imp_implementationWithBlock(keyboardWillShow), method_getTypeEncoding(method2)) 231 | } 232 | 233 | objc_registerClassPair(viewSubclass) 234 | object_setClass(view, viewSubclass) 235 | } 236 | } 237 | 238 | override init(rootView: Content) 239 | { 240 | super.init(rootView: rootView) 241 | disableSafeArea() 242 | disableInteractionsIfNeeded() 243 | } 244 | 245 | @available(*, unavailable) 246 | @objc dynamic required init?(coder aDecoder: NSCoder) 247 | { 248 | fatalError("init(coder:) has not been implemented") 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Implementation/ASSection.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public struct ASCollectionViewStaticContent: Identifiable 8 | { 9 | public var index: Int 10 | var view: AnyView 11 | 12 | public var id: Int { index } 13 | } 14 | 15 | @available(iOS 13.0, *) 16 | public struct ASCollectionViewItemUniqueID: Hashable 17 | { 18 | var sectionIDHash: Int 19 | var itemIDHash: Int 20 | init(sectionID: SectionID, itemID: ItemID) 21 | { 22 | sectionIDHash = sectionID.hashValue 23 | itemIDHash = itemID.hashValue 24 | } 25 | } 26 | 27 | @available(iOS 13.0, *) 28 | public typealias ASCollectionViewSection = ASSection 29 | 30 | @available(iOS 13.0, *) 31 | public struct ASSection 32 | { 33 | public var id: SectionID 34 | 35 | internal var dataSource: ASSectionDataSourceProtocol 36 | 37 | public var itemIDs: [ASCollectionViewItemUniqueID] 38 | { 39 | dataSource.getUniqueItemIDs(withSectionID: id) 40 | } 41 | 42 | var shouldCacheCells: Bool = false 43 | 44 | // Only relevant for ASTableView 45 | var sectionIndexTitle: String? 46 | var disableDefaultTheming: Bool = false 47 | var tableViewSeparatorInsets: UIEdgeInsets? 48 | var estimatedHeaderHeight: CGFloat? 49 | var estimatedFooterHeight: CGFloat? 50 | } 51 | 52 | // MARK: SUPPLEMENTARY VIEWS - INTERNAL 53 | 54 | @available(iOS 13.0, *) 55 | internal extension ASCollectionViewSection 56 | { 57 | mutating func setHeaderView(_ view: Content?) 58 | { 59 | setSupplementaryView(view, ofKind: UICollectionView.elementKindSectionHeader) 60 | } 61 | 62 | mutating func setFooterView(_ view: Content?) 63 | { 64 | setSupplementaryView(view, ofKind: UICollectionView.elementKindSectionFooter) 65 | } 66 | 67 | mutating func setSupplementaryView(_ view: Content?, ofKind kind: String) 68 | { 69 | guard let view = view 70 | else 71 | { 72 | dataSource.supplementaryViews.removeValue(forKey: kind) 73 | return 74 | } 75 | 76 | dataSource.supplementaryViews[kind] = AnyView(view) 77 | } 78 | 79 | var supplementaryKinds: Set 80 | { 81 | Set(dataSource.supplementaryViews.keys) 82 | } 83 | 84 | func supplementary(ofKind kind: String) -> AnyView? 85 | { 86 | dataSource.supplementaryViews[kind] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/ASIndexedDictionary.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | struct ASIndexedDictionary: BidirectionalCollection 6 | { 7 | private var dictionary: [Key: Int] = [:] 8 | private var array: [Value] = [] 9 | 10 | mutating func append(_ item: (key: Key, value: Value)) 11 | { 12 | if let index = dictionary[item.key] 13 | { 14 | array.remove(at: index) 15 | } 16 | array.append(item.value) 17 | dictionary[item.key] = array.endIndex - 1 18 | } 19 | 20 | mutating func append(_ items: [(key: Key, value: Value)]) 21 | { 22 | items.forEach { append($0) } 23 | } 24 | 25 | mutating func removeAll() 26 | { 27 | dictionary.removeAll() 28 | array.removeAll() 29 | } 30 | 31 | var startIndex: Int { array.startIndex } 32 | 33 | var endIndex: Int { array.endIndex } 34 | 35 | var lastIndex: Int { Swift.max(startIndex, endIndex - 1) } 36 | 37 | func index(before i: Int) -> Int 38 | { 39 | array.index(before: i) 40 | } 41 | 42 | func index(after i: Int) -> Int 43 | { 44 | array.index(after: i) 45 | } 46 | 47 | subscript(index: Int) -> Value 48 | { 49 | array[index] 50 | } 51 | 52 | subscript(_ key: Key) -> Value? 53 | { 54 | get 55 | { 56 | dictionary[key].map { array[$0] } 57 | } 58 | set 59 | { 60 | guard let newValue = newValue 61 | else 62 | { 63 | _ = dictionary[key].map { array.remove(at: $0) } 64 | return 65 | } 66 | if let index = dictionary[key] 67 | { 68 | array[index] = newValue 69 | } 70 | else 71 | { 72 | append((key, value: newValue)) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/ASOptionalSize.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import CoreGraphics 4 | 5 | struct ASOptionalSize 6 | { 7 | let width: CGFloat? 8 | let height: CGFloat? 9 | 10 | init(width: CGFloat? = nil, height: CGFloat? = nil) 11 | { 12 | self.width = width 13 | self.height = height 14 | } 15 | 16 | init(_ size: CGSize) 17 | { 18 | width = size.width 19 | height = size.height 20 | } 21 | 22 | static let none = ASOptionalSize() 23 | } 24 | 25 | extension CGFloat 26 | { 27 | func applyOptionalMinBound(_ optionalMinBound: CGFloat?) -> CGFloat 28 | { 29 | optionalMinBound.map { Swift.max($0, self) } ?? self 30 | } 31 | 32 | func applyOptionalMaxBound(_ optionalMaxBound: CGFloat?) -> CGFloat 33 | { 34 | optionalMaxBound.map { Swift.min($0, self) } ?? self 35 | } 36 | } 37 | 38 | extension CGSize 39 | { 40 | func applyMinSize(_ minSize: ASOptionalSize) -> CGSize 41 | { 42 | CGSize( 43 | width: width.applyOptionalMinBound(minSize.width), 44 | height: height.applyOptionalMinBound(minSize.height)) 45 | } 46 | 47 | func applyMaxSize(_ maxSize: ASOptionalSize) -> CGSize 48 | { 49 | CGSize( 50 | width: width.applyOptionalMaxBound(maxSize.width), 51 | height: height.applyOptionalMaxBound(maxSize.height)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/ASPriorityCache.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | public final class ASPriorityCache 6 | { 7 | var maxSize: Int? = 50 8 | 9 | private var nodes: [Key: Node] = [:] 10 | private var head: Node? 11 | private var tail: Node? 12 | 13 | private class Node 14 | { 15 | var key: Key 16 | var value: Value 17 | var next: Node? 18 | weak var previous: Node? 19 | 20 | init(key: Key, value: Value) 21 | { 22 | self.key = key 23 | self.value = value 24 | } 25 | } 26 | 27 | private func appendNode(_ node: Node) 28 | { 29 | nodes[node.key] = node 30 | if let oldTail = tail 31 | { 32 | oldTail.next = node 33 | node.previous = oldTail 34 | tail = node 35 | } 36 | else 37 | { 38 | head = node 39 | tail = node 40 | } 41 | maxSize.map 42 | { 43 | if nodes.count > $0 44 | { 45 | removeFirstNode() 46 | } 47 | } 48 | } 49 | 50 | private func removeNode(_ node: Node) 51 | { 52 | node.previous?.next = node.next 53 | node.next?.previous = node.previous 54 | if node === head 55 | { 56 | head = node.next 57 | } 58 | if node === tail 59 | { 60 | tail = node.previous 61 | } 62 | nodes[node.key] = nil 63 | } 64 | 65 | private func removeFirstNode() 66 | { 67 | head.map(removeNode) 68 | } 69 | 70 | private func removeLastNode() 71 | { 72 | tail.map(removeNode) 73 | } 74 | 75 | private func moveNodeToLast(_ node: Node) 76 | { 77 | guard node !== tail else { return } 78 | removeNode(node) 79 | appendNode(node) 80 | } 81 | } 82 | 83 | public extension ASPriorityCache 84 | { 85 | var first: Value? 86 | { 87 | head?.value 88 | } 89 | 90 | var last: Value? 91 | { 92 | tail?.value 93 | } 94 | 95 | subscript(_ key: Key) -> Value? 96 | { 97 | get { nodes[key]?.value } 98 | set 99 | { 100 | guard let newValue = newValue 101 | else 102 | { 103 | nodes[key].map(removeNode) 104 | return 105 | } 106 | if let node = nodes[key] 107 | { 108 | node.value = newValue 109 | moveNodeToLast(node) 110 | } 111 | else 112 | { 113 | let node = Node(key: key, value: newValue) 114 | appendNode(node) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/ASSelfSizingSettings.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | public struct ASSelfSizingContext 6 | { 7 | public enum CellType 8 | { 9 | case content 10 | case supplementary(String) 11 | } 12 | 13 | public let cellType: CellType 14 | public let indexPath: IndexPath 15 | } 16 | 17 | public struct ASSelfSizingConfig 18 | { 19 | public init(selfSizeHorizontally: Bool? = nil, selfSizeVertically: Bool? = nil, canExceedCollectionWidth: Bool = true, canExceedCollectionHeight: Bool = true) 20 | { 21 | self.selfSizeHorizontally = selfSizeHorizontally 22 | self.selfSizeVertically = selfSizeVertically 23 | self.canExceedCollectionWidth = canExceedCollectionWidth 24 | self.canExceedCollectionHeight = canExceedCollectionHeight 25 | } 26 | 27 | var selfSizeHorizontally: Bool? 28 | var selfSizeVertically: Bool? 29 | var canExceedCollectionWidth: Bool 30 | var canExceedCollectionHeight: Bool 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/Binding+Sequence.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public extension Binding where Value == [Int: Set] 8 | { 9 | subscript(index: Int) -> Binding> 10 | { 11 | Binding>(get: { 12 | self.wrappedValue[index] ?? [] 13 | }, set: { 14 | self.wrappedValue[index] = $0 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/GlobalConvenienceFunctions.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | @discardableResult 6 | func assignIfChanged(_ object: Object, _ keyPath: ReferenceWritableKeyPath, newValue: T) -> Bool 7 | { 8 | guard newValue != object[keyPath: keyPath] else { return false } 9 | object[keyPath: keyPath] = newValue 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/RandomAccessCollection+Safe.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | 5 | extension RandomAccessCollection 6 | { 7 | public func containsIndex(_ index: Index) -> Bool 8 | { 9 | indices.contains(index) 10 | } 11 | 12 | subscript(safe index: Index) -> Element? 13 | { 14 | guard containsIndex(index) else { return nil } 15 | return self[index] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/Support/ShrinkToFitWrapper.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import SwiftUI 4 | 5 | @available(iOS 13.0, *) 6 | protocol ContentSize 7 | { 8 | var contentSizeTracker: ContentSizeTracker? { get set } 9 | } 10 | 11 | @available(iOS 13.0, *) 12 | public enum ShrinkDimension 13 | { 14 | case horizontal 15 | case vertical 16 | 17 | var shrinkVertical: Bool 18 | { 19 | self == .vertical 20 | } 21 | 22 | var shrinkHorizontal: Bool 23 | { 24 | self == .horizontal 25 | } 26 | } 27 | 28 | @available(iOS 13.0, *) 29 | struct SelfSizingWrapper: View 30 | { 31 | @State var contentSizeTracker = ContentSizeTracker() 32 | 33 | var content: Content 34 | var shrinkDirection: ShrinkDimension 35 | var isEnabled: Bool = true 36 | var expandToFitMode: Bool = false 37 | 38 | var modifiedContent: Content 39 | { 40 | var content = self.content 41 | content.contentSizeTracker = contentSizeTracker 42 | return content 43 | } 44 | 45 | var body: some View 46 | { 47 | SubWrapper(contentSizeTracker: contentSizeTracker, content: modifiedContent, shrinkDirection: shrinkDirection, isEnabled: isEnabled, expandToFitMode: expandToFitMode) 48 | } 49 | } 50 | 51 | @available(iOS 13.0, *) 52 | struct SubWrapper: View 53 | { 54 | @ObservedObject 55 | var contentSizeTracker: ContentSizeTracker 56 | 57 | var content: Content 58 | var shrinkDirection: ShrinkDimension 59 | var isEnabled: Bool 60 | var expandToFitMode: Bool 61 | 62 | var body: some View 63 | { 64 | content 65 | .frame( 66 | minWidth: isEnabled && expandToFitMode && shrinkDirection.shrinkHorizontal ? contentSizeTracker.contentSize?.width : nil, 67 | idealWidth: isEnabled && shrinkDirection.shrinkHorizontal ? contentSizeTracker.contentSize?.width : nil, 68 | maxWidth: expandToFitMode ? .infinity : (isEnabled && shrinkDirection.shrinkHorizontal ? contentSizeTracker.contentSize?.width : nil), 69 | minHeight: isEnabled && expandToFitMode && shrinkDirection.shrinkVertical ? contentSizeTracker.contentSize?.height : nil, 70 | idealHeight: isEnabled && shrinkDirection.shrinkVertical ? contentSizeTracker.contentSize?.height : nil, 71 | maxHeight: expandToFitMode ? .infinity : (isEnabled && shrinkDirection.shrinkVertical ? contentSizeTracker.contentSize?.height : nil), 72 | alignment: .topLeading) 73 | } 74 | } 75 | 76 | @available(iOS 13.0, *) 77 | class ContentSizeTracker: ObservableObject 78 | { 79 | @Published 80 | var contentSize: CGSize? 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/UIKit/AS_UICollectionView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public class AS_CollectionViewController: UIViewController 8 | { 9 | weak var coordinator: ASCollectionViewCoordinator? 10 | { 11 | didSet 12 | { 13 | collectionView.coordinator = coordinator 14 | } 15 | } 16 | 17 | var collectionViewLayout: UICollectionViewLayout 18 | lazy var collectionView: AS_UICollectionView = { 19 | let cv = AS_UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 20 | cv.coordinator = coordinator 21 | return cv 22 | }() 23 | 24 | public init(collectionViewLayout layout: UICollectionViewLayout) 25 | { 26 | collectionViewLayout = layout 27 | super.init(nibName: nil, bundle: nil) 28 | } 29 | 30 | @available(*, unavailable) 31 | required init?(coder: NSCoder) 32 | { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override public func loadView() 37 | { 38 | view = collectionView 39 | } 40 | 41 | override public func viewDidLoad() 42 | { 43 | super.viewDidLoad() 44 | view.backgroundColor = .clear 45 | } 46 | 47 | override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 48 | { 49 | // Get current central cell 50 | self.coordinator?.prepareForOrientationChange() 51 | 52 | 53 | coordinator.animate(alongsideTransition: { _ in 54 | self.view.setNeedsLayout() 55 | self.view.layoutIfNeeded() 56 | if 57 | let desiredOffset = self.coordinator?.getContentOffsetForOrientationChange(), 58 | self.collectionView.contentOffset != desiredOffset 59 | { 60 | self.collectionView.contentOffset = desiredOffset 61 | } 62 | }) 63 | { _ in 64 | // Completion 65 | self.coordinator?.completedOrientationChange() 66 | } 67 | 68 | super.viewWillTransition(to: size, with: coordinator) 69 | } 70 | 71 | override public func viewSafeAreaInsetsDidChange() 72 | { 73 | super.viewSafeAreaInsetsDidChange() 74 | // The following is a workaround to fix the interface rotation animation under SwiftUI 75 | collectionViewLayout.invalidateLayout() 76 | } 77 | 78 | override public func viewDidLayoutSubviews() 79 | { 80 | super.viewDidLayoutSubviews() 81 | coordinator?.didUpdateContentSize(collectionView.contentSize) 82 | } 83 | } 84 | 85 | @available(iOS 13.0, *) 86 | class AS_UICollectionView: UICollectionView 87 | { 88 | weak var coordinator: ASCollectionViewCoordinator? 89 | override func didMoveToWindow() 90 | { 91 | if window != nil 92 | { 93 | coordinator?.onMoveToParent() 94 | } 95 | else 96 | { 97 | coordinator?.onMoveFromParent() 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/UIKit/AS_UITableView.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import SwiftUI 5 | 6 | @available(iOS 13.0, *) 7 | public class AS_TableViewController: UIViewController 8 | { 9 | weak var coordinator: ASTableViewCoordinator? 10 | { 11 | didSet 12 | { 13 | tableView.coordinator = coordinator 14 | } 15 | } 16 | 17 | var style: UITableView.Style 18 | 19 | lazy var tableView: AS_UITableView = { 20 | let tableView = AS_UITableView(frame: .zero, style: style) 21 | tableView.coordinator = coordinator 22 | tableView.tableHeaderView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: CGFloat.leastNormalMagnitude, height: CGFloat.leastNormalMagnitude))) // Remove unnecessary padding in Style.grouped/insetGrouped 23 | tableView.tableFooterView = UIView(frame: CGRect(origin: .zero, size: CGSize(width: CGFloat.leastNormalMagnitude, height: CGFloat.leastNormalMagnitude))) // Remove separators for non-existent cells 24 | return tableView 25 | }() 26 | 27 | public init(style: UITableView.Style) 28 | { 29 | self.style = style 30 | super.init(nibName: nil, bundle: nil) 31 | } 32 | 33 | @available(*, unavailable) 34 | required init?(coder: NSCoder) 35 | { 36 | fatalError("init(coder:) has not been implemented") 37 | } 38 | 39 | override public func loadView() 40 | { 41 | view = tableView 42 | } 43 | 44 | override public func viewDidLoad() 45 | { 46 | super.viewDidLoad() 47 | } 48 | 49 | override public func viewDidLayoutSubviews() 50 | { 51 | super.viewDidLayoutSubviews() 52 | coordinator?.didUpdateContentSize(tableView.contentSize) 53 | } 54 | } 55 | 56 | @available(iOS 13.0, *) 57 | class AS_UITableView: UITableView 58 | { 59 | weak var coordinator: ASTableViewCoordinator? 60 | override func didMoveToWindow() 61 | { 62 | if window != nil 63 | { 64 | coordinator?.onMoveToParent() 65 | } 66 | else 67 | { 68 | coordinator?.onMoveFromParent() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/UIKitExtensions/UICollectionView+Convenience.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import UIKit 5 | 6 | extension UICollectionView 7 | { 8 | var allSections: Range? 9 | { 10 | let sectionCount = dataSource?.numberOfSections?(in: self) ?? 1 11 | guard sectionCount > 0 else { return nil } 12 | return (0 ..< sectionCount) 13 | } 14 | 15 | func allIndexPaths(inSection section: Int) -> [IndexPath] 16 | { 17 | guard let itemCount = dataSource?.collectionView(self, numberOfItemsInSection: section), itemCount > 0 else { return [] } 18 | return (0 ..< itemCount).map 19 | { item in 20 | IndexPath(item: item, section: section) 21 | } 22 | } 23 | 24 | func allIndexPaths() -> [IndexPath] 25 | { 26 | guard let allSections = allSections else { return [] } 27 | return allSections.flatMap 28 | { section -> [IndexPath] in 29 | allIndexPaths(inSection: section) 30 | } 31 | } 32 | 33 | func allIndexPaths(after afterIndexPath: IndexPath) -> [IndexPath] 34 | { 35 | guard let sectionCount = dataSource?.numberOfSections?(in: self), sectionCount > 0 else { return [] } 36 | return (afterIndexPath.section ..< sectionCount).flatMap 37 | { section -> [IndexPath] in 38 | guard let itemCount = dataSource?.collectionView(self, numberOfItemsInSection: section), itemCount > 0 else { return [] } 39 | let startIndex: Int 40 | if section == afterIndexPath.section 41 | { 42 | startIndex = afterIndexPath.item + 1 43 | } 44 | else 45 | { 46 | startIndex = 0 47 | } 48 | guard startIndex < itemCount else { return [] } 49 | return (startIndex ..< itemCount).map 50 | { item in 51 | IndexPath(item: item, section: section) 52 | } 53 | } 54 | } 55 | 56 | var firstIndexPath: IndexPath? 57 | { 58 | guard 59 | let sectionCount = dataSource?.numberOfSections?(in: self), sectionCount > 0, 60 | let itemCount = dataSource?.collectionView(self, numberOfItemsInSection: 0), itemCount > 0 61 | else { return nil } 62 | return IndexPath(item: 0, section: 0) 63 | } 64 | 65 | var lastIndexPath: IndexPath? 66 | { 67 | guard 68 | let sectionCount = dataSource?.numberOfSections?(in: self), sectionCount > 0, 69 | let itemCount = dataSource?.collectionView(self, numberOfItemsInSection: sectionCount - 1), itemCount > 0 70 | else { return nil } 71 | return IndexPath(item: itemCount - 1, section: sectionCount - 1) 72 | } 73 | } 74 | 75 | public enum Boundary: CaseIterable 76 | { 77 | case left 78 | case right 79 | case top 80 | case bottom 81 | } 82 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/UIKitExtensions/UIScrollView+Convenience.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import UIKit 4 | 5 | extension UIScrollView 6 | { 7 | var contentSizePlusInsets: CGSize 8 | { 9 | CGSize( 10 | width: contentSize.width + adjustedContentInset.left + adjustedContentInset.right, 11 | height: contentSize.height + adjustedContentInset.bottom + adjustedContentInset.top) 12 | } 13 | 14 | var maxContentOffset: CGPoint 15 | { 16 | CGPoint( 17 | x: max(0, contentSizePlusInsets.width - bounds.width), 18 | y: max(-adjustedContentInset.top, contentSizePlusInsets.height - bounds.height)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/ASCollectionView/UIKitExtensions/UIView+Convenience.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import Foundation 4 | import UIKit 5 | 6 | extension UIView 7 | { 8 | func findFirstResponder() -> UIView? 9 | { 10 | if isFirstResponder 11 | { 12 | return self 13 | } 14 | else 15 | { 16 | for subview in subviews 17 | { 18 | if let found = subview.findFirstResponder() 19 | { 20 | return found 21 | } 22 | } 23 | } 24 | return nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /readmeAssets/SampleUsage.swift: -------------------------------------------------------------------------------- 1 | // ASCollectionView. Created by Apptek Studios 2019 2 | 3 | import ASCollectionView 4 | import SwiftUI 5 | 6 | struct SingleSectionExampleView: View 7 | { 8 | @State var dataExample = (0 ..< 21).map { $0 } 9 | 10 | var body: some View 11 | { 12 | ASCollectionView(data: dataExample, dataID: \.self) 13 | { item, _ in 14 | Color.blue 15 | .overlay(Text("\(item)")) 16 | } 17 | .layout 18 | { 19 | .grid( 20 | layoutMode: .adaptive(withMinItemSize: 100), 21 | itemSpacing: 5, 22 | lineSpacing: 5, 23 | itemSize: .absolute(50)) 24 | } 25 | } 26 | } 27 | 28 | struct ExampleView: View 29 | { 30 | @State var dataExampleA = (0 ..< 21).map { $0 } 31 | @State var dataExampleB = (0 ..< 15).map { "ITEM \($0)" } 32 | 33 | var body: some View 34 | { 35 | ASCollectionView 36 | { 37 | ASSection( 38 | id: 0, 39 | data: dataExampleA, 40 | dataID: \.self) 41 | { item, _ in 42 | Color.blue 43 | .overlay( 44 | Text("\(item)") 45 | ) 46 | } 47 | ASSection( 48 | id: 1, 49 | data: dataExampleB, 50 | dataID: \.self) 51 | { item, _ in 52 | Color.green 53 | .overlay( 54 | Text("Complex layout - \(item)") 55 | ) 56 | } 57 | .sectionHeader 58 | { 59 | Text("Section header") 60 | .padding() 61 | .frame(maxWidth: .infinity, alignment: .leading) 62 | .background(Color.yellow) 63 | } 64 | .sectionFooter 65 | { 66 | Text("This is a section footer!") 67 | .padding() 68 | } 69 | } 70 | .layout 71 | { sectionID in 72 | switch sectionID 73 | { 74 | case 0: 75 | return .grid( 76 | layoutMode: .adaptive(withMinItemSize: 100), 77 | itemSpacing: 5, 78 | lineSpacing: 5, 79 | itemSize: .absolute(50)) 80 | default: 81 | return ASCollectionLayoutSection 82 | { environment in 83 | let isWide = environment.container.effectiveContentSize.width > 500 84 | let gridBlockSize = environment.container.effectiveContentSize.width / (isWide ? 5 : 3) 85 | let gridItemInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) 86 | let itemSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize), heightDimension: .absolute(gridBlockSize)) 87 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 88 | item.contentInsets = gridItemInsets 89 | let verticalGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize), heightDimension: .absolute(gridBlockSize * 2)) 90 | let verticalGroup = NSCollectionLayoutGroup.vertical(layoutSize: verticalGroupSize, subitem: item, count: 2) 91 | 92 | let featureItemSize = NSCollectionLayoutSize(widthDimension: .absolute(gridBlockSize * 2), heightDimension: .absolute(gridBlockSize * 2)) 93 | let featureItem = NSCollectionLayoutItem(layoutSize: featureItemSize) 94 | featureItem.contentInsets = gridItemInsets 95 | 96 | let verticalAndFeatureGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize * 2)) 97 | let verticalAndFeatureGroupA = NSCollectionLayoutGroup.horizontal(layoutSize: verticalAndFeatureGroupSize, subitems: isWide ? [verticalGroup, verticalGroup, featureItem, verticalGroup] : [verticalGroup, featureItem]) 98 | let verticalAndFeatureGroupB = NSCollectionLayoutGroup.horizontal(layoutSize: verticalAndFeatureGroupSize, subitems: isWide ? [verticalGroup, featureItem, verticalGroup, verticalGroup] : [featureItem, verticalGroup]) 99 | 100 | let rowGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize)) 101 | let rowGroup = NSCollectionLayoutGroup.horizontal(layoutSize: rowGroupSize, subitem: item, count: isWide ? 5 : 3) 102 | 103 | let outerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(gridBlockSize * 6)) 104 | let outerGroup = NSCollectionLayoutGroup.vertical(layoutSize: outerGroupSize, subitems: [verticalAndFeatureGroupA, rowGroup, verticalAndFeatureGroupB, rowGroup]) 105 | 106 | let section = NSCollectionLayoutSection(group: outerGroup) 107 | 108 | let supplementarySize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) 109 | let headerSupplementary = NSCollectionLayoutBoundarySupplementaryItem( 110 | layoutSize: supplementarySize, 111 | elementKind: UICollectionView.elementKindSectionHeader, 112 | alignment: .top) 113 | let footerSupplementary = NSCollectionLayoutBoundarySupplementaryItem( 114 | layoutSize: supplementarySize, 115 | elementKind: UICollectionView.elementKindSectionFooter, 116 | alignment: .bottom) 117 | section.boundarySupplementaryItems = [headerSupplementary, footerSupplementary] 118 | return section 119 | } 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /readmeAssets/demo1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo1.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo2.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo3.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo4.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo5.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo6.jpeg -------------------------------------------------------------------------------- /readmeAssets/demo7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apptekstudios/ASCollectionView/561ee73a16508868f2448caba1da2d72dbf662b2/readmeAssets/demo7.jpeg --------------------------------------------------------------------------------