├── .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