├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .spi.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── AdvancedList.xcscheme
├── .travis.yml
├── Example
├── AdvancedList-Example.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── AdvancedListExample (iOS).xcscheme
├── AdvancedList.gif
├── LICENSE
├── README.md
├── Shared
│ ├── AdvancedListExampleApp.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ └── restaurant.imageset
│ │ │ ├── Contents.json
│ │ │ └── restaurant.jpg
│ ├── ExampleDataProvider.swift
│ ├── Models
│ │ ├── AdListItem
│ │ │ ├── AdDetailView.swift
│ │ │ ├── AdListItem.swift
│ │ │ ├── AdListItemView.swift
│ │ │ └── AdListItemViewRepresentationType.swift
│ │ ├── ContactListItem
│ │ │ ├── ContactDetailView.swift
│ │ │ ├── ContactListItem.swift
│ │ │ ├── ContactListItemView.swift
│ │ │ ├── ContactListItemViewRepresentationType.swift
│ │ │ └── ContactView.swift
│ │ └── ExampleError.swift
│ └── Views
│ │ ├── ContentExampleView.swift
│ │ ├── CustomListStateSegmentedControlView.swift
│ │ ├── DataExampleView.swift
│ │ └── ListContentExampleView.swift
├── iOS
│ └── Info.plist
└── macOS
│ ├── Info.plist
│ └── macOS.entitlements
├── LICENSE
├── Package.resolved
├── Package.swift
├── README.md
├── Sources
└── AdvancedList
│ ├── private
│ └── Models
│ │ ├── AdvancedListType.swift
│ │ ├── AnyAdvancedListPagination.swift
│ │ └── ListState+error.swift
│ └── public
│ ├── Models
│ ├── AdvancedListPagination.swift
│ ├── AdvancedListPaginationState.swift
│ ├── AdvancedListPaginationType.swift
│ ├── AnyDynamicViewContent.swift
│ ├── AnyIdentifiable.swift
│ └── ListState.swift
│ └── Views
│ └── AdvancedList.swift
└── Tests
├── AdvancedListTests
├── AdvancedListPaginationStateTests.swift
├── AdvancedListTests.swift
├── Extensions
│ ├── AdvancedList+Inspectable.swift
│ ├── AnyDynamicViewContent+Inspectable.swift
│ └── String+Identifiable.swift
├── ListStateTests.swift
└── XCTestManifests.swift
└── LinuxMain.swift
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | name: Test and upload coverage data
12 | runs-on: macos-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Test
17 | run: xcodebuild clean test -destination 'name=iPhone 8' -scheme AdvancedList -enableCodeCoverage YES -derivedDataPath .build/derivedData -quiet
18 | - name: Upload Test coverage data
19 | run: bash <(curl -s https://codecov.io/bash) -J '^AdvancedList$' -D .build/derivedData
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | builder:
3 | configs:
4 | - documentation_targets: [AdvancedList]
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/AdvancedList.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: swift
2 | osx_image: xcode12
3 | script:
4 | - swift package generate-xcodeproj
5 | - xcodebuild clean test -destination 'name=iPhone 8' -scheme AdvancedList-Package -enableCodeCoverage YES -derivedDataPath .build/derivedData -quiet
6 | after_success:
7 | # upload test coverage data
8 | - bash <(curl -s https://codecov.io/bash) -J '^AdvancedList$' -D .build/derivedData
--------------------------------------------------------------------------------
/Example/AdvancedList-Example.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | B9B5A11B27F4EF74002FD9A0 /* ContentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5A11A27F4EF74002FD9A0 /* ContentExampleView.swift */; };
11 | B9B5A11C27F4EF74002FD9A0 /* ContentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5A11A27F4EF74002FD9A0 /* ContentExampleView.swift */; };
12 | B9BFF1DC24D777BD005C9C21 /* AdvancedListExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BFF1CA24D777BC005C9C21 /* AdvancedListExampleApp.swift */; };
13 | B9BFF1DD24D777BD005C9C21 /* AdvancedListExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9BFF1CA24D777BC005C9C21 /* AdvancedListExampleApp.swift */; };
14 | B9BFF1E024D777BD005C9C21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9BFF1CC24D777BD005C9C21 /* Assets.xcassets */; };
15 | B9BFF1E124D777BD005C9C21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9BFF1CC24D777BD005C9C21 /* Assets.xcassets */; };
16 | B9BFF1E924D7788B005C9C21 /* ExampleDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAD564622D75C4500FB141B /* ExampleDataProvider.swift */; };
17 | B9BFF1EA24D7788B005C9C21 /* ExampleDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAD564622D75C4500FB141B /* ExampleDataProvider.swift */; };
18 | B9BFF1EB24D778A2005C9C21 /* DataExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9DFEB22CA8CDC00051FE2 /* DataExampleView.swift */; };
19 | B9BFF1EC24D778A2005C9C21 /* DataExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9DFEB22CA8CDC00051FE2 /* DataExampleView.swift */; };
20 | B9BFF1EF24D778CD005C9C21 /* ExampleError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A4A22D3C99000063925 /* ExampleError.swift */; };
21 | B9BFF1F024D778CD005C9C21 /* ExampleError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A4A22D3C99000063925 /* ExampleError.swift */; };
22 | B9BFF1F124D778D1005C9C21 /* AdListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DA7AA423A2D8E4003C615B /* AdListItemView.swift */; };
23 | B9BFF1F224D778D1005C9C21 /* AdListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA92D2422D6127C000C3F7A /* AdListItem.swift */; };
24 | B9BFF1F324D778D1005C9C21 /* AdDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984A922D90FC3002407EF /* AdDetailView.swift */; };
25 | B9BFF1F424D778D1005C9C21 /* AdListItemViewRepresentationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984AF22D91090002407EF /* AdListItemViewRepresentationType.swift */; };
26 | B9BFF1F524D778D1005C9C21 /* AdListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DA7AA423A2D8E4003C615B /* AdListItemView.swift */; };
27 | B9BFF1F624D778D1005C9C21 /* AdListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA92D2422D6127C000C3F7A /* AdListItem.swift */; };
28 | B9BFF1F724D778D1005C9C21 /* AdDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984A922D90FC3002407EF /* AdDetailView.swift */; };
29 | B9BFF1F824D778D1005C9C21 /* AdListItemViewRepresentationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984AF22D91090002407EF /* AdListItemViewRepresentationType.swift */; };
30 | B9BFF1F924D778D5005C9C21 /* ContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A5022DA167E00063925 /* ContactView.swift */; };
31 | B9BFF1FA24D778D5005C9C21 /* ContactListItemViewRepresentationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984AD22D91066002407EF /* ContactListItemViewRepresentationType.swift */; };
32 | B9BFF1FB24D778D5005C9C21 /* ContactDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A4E22DA153900063925 /* ContactDetailView.swift */; };
33 | B9BFF1FC24D778D5005C9C21 /* ContactListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9E00B22CA8E5C00051FE2 /* ContactListItem.swift */; };
34 | B9BFF1FD24D778D5005C9C21 /* ContactListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DA7AA223A2D7F9003C615B /* ContactListItemView.swift */; };
35 | B9BFF1FE24D778D5005C9C21 /* ContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A5022DA167E00063925 /* ContactView.swift */; };
36 | B9BFF1FF24D778D5005C9C21 /* ContactListItemViewRepresentationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2984AD22D91066002407EF /* ContactListItemViewRepresentationType.swift */; };
37 | B9BFF20024D778D5005C9C21 /* ContactDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9884A4E22DA153900063925 /* ContactDetailView.swift */; };
38 | B9BFF20124D778D5005C9C21 /* ContactListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9D9E00B22CA8E5C00051FE2 /* ContactListItem.swift */; };
39 | B9BFF20224D778D5005C9C21 /* ContactListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DA7AA223A2D7F9003C615B /* ContactListItemView.swift */; };
40 | B9BFF20324D778DA005C9C21 /* CustomListStateSegmentedControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAD564822D7C26300FB141B /* CustomListStateSegmentedControlView.swift */; };
41 | B9BFF20424D778DA005C9C21 /* CustomListStateSegmentedControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAD564822D7C26300FB141B /* CustomListStateSegmentedControlView.swift */; };
42 | B9BFF20624D778F5005C9C21 /* AdvancedList in Frameworks */ = {isa = PBXBuildFile; productRef = B9BFF20524D778F5005C9C21 /* AdvancedList */; };
43 | B9BFF20824D77C28005C9C21 /* AdvancedList in Frameworks */ = {isa = PBXBuildFile; productRef = B9BFF20724D77C28005C9C21 /* AdvancedList */; };
44 | B9FDFA822803113C0079466B /* ListContentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FDFA812803113C0079466B /* ListContentExampleView.swift */; };
45 | B9FDFA832803113C0079466B /* ListContentExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FDFA812803113C0079466B /* ListContentExampleView.swift */; };
46 | /* End PBXBuildFile section */
47 |
48 | /* Begin PBXFileReference section */
49 | B9884A4A22D3C99000063925 /* ExampleError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleError.swift; sourceTree = ""; };
50 | B9884A4E22DA153900063925 /* ContactDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactDetailView.swift; sourceTree = ""; };
51 | B9884A5022DA167E00063925 /* ContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactView.swift; sourceTree = ""; };
52 | B99D5FA127A70FDB000F3688 /* AdvancedList */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AdvancedList; path = ..; sourceTree = ""; };
53 | B9B5A11A27F4EF74002FD9A0 /* ContentExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentExampleView.swift; sourceTree = ""; };
54 | B9B7E17623049FD60018BD33 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
55 | B9BFF1CA24D777BC005C9C21 /* AdvancedListExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedListExampleApp.swift; sourceTree = ""; };
56 | B9BFF1CC24D777BD005C9C21 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
57 | B9BFF1D124D777BD005C9C21 /* AdvancedListExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvancedListExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
58 | B9BFF1D324D777BD005C9C21 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
59 | B9BFF1D824D777BD005C9C21 /* AdvancedListExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdvancedListExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
60 | B9BFF1DA24D777BD005C9C21 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
61 | B9BFF1DB24D777BD005C9C21 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
62 | B9D9DFEB22CA8CDC00051FE2 /* DataExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExampleView.swift; sourceTree = ""; };
63 | B9D9E00B22CA8E5C00051FE2 /* ContactListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListItem.swift; sourceTree = ""; };
64 | B9D9E01222CA971D00051FE2 /* AdvancedList.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = AdvancedList.gif; sourceTree = SOURCE_ROOT; };
65 | B9DA7AA223A2D7F9003C615B /* ContactListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListItemView.swift; sourceTree = ""; };
66 | B9DA7AA423A2D8E4003C615B /* AdListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdListItemView.swift; sourceTree = ""; };
67 | B9FDFA812803113C0079466B /* ListContentExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListContentExampleView.swift; sourceTree = ""; };
68 | CE2984A922D90FC3002407EF /* AdDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdDetailView.swift; sourceTree = ""; };
69 | CE2984AD22D91066002407EF /* ContactListItemViewRepresentationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListItemViewRepresentationType.swift; sourceTree = ""; };
70 | CE2984AF22D91090002407EF /* AdListItemViewRepresentationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdListItemViewRepresentationType.swift; sourceTree = ""; };
71 | CEA92D2422D6127C000C3F7A /* AdListItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdListItem.swift; sourceTree = ""; };
72 | CEAD564622D75C4500FB141B /* ExampleDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleDataProvider.swift; sourceTree = ""; };
73 | CEAD564822D7C26300FB141B /* CustomListStateSegmentedControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListStateSegmentedControlView.swift; sourceTree = ""; };
74 | /* End PBXFileReference section */
75 |
76 | /* Begin PBXFrameworksBuildPhase section */
77 | B9BFF1CE24D777BD005C9C21 /* Frameworks */ = {
78 | isa = PBXFrameworksBuildPhase;
79 | buildActionMask = 2147483647;
80 | files = (
81 | B9BFF20624D778F5005C9C21 /* AdvancedList in Frameworks */,
82 | );
83 | runOnlyForDeploymentPostprocessing = 0;
84 | };
85 | B9BFF1D524D777BD005C9C21 /* Frameworks */ = {
86 | isa = PBXFrameworksBuildPhase;
87 | buildActionMask = 2147483647;
88 | files = (
89 | B9BFF20824D77C28005C9C21 /* AdvancedList in Frameworks */,
90 | );
91 | runOnlyForDeploymentPostprocessing = 0;
92 | };
93 | /* End PBXFrameworksBuildPhase section */
94 |
95 | /* Begin PBXGroup section */
96 | B9B7E173230440850018BD33 /* Frameworks */ = {
97 | isa = PBXGroup;
98 | children = (
99 | );
100 | name = Frameworks;
101 | sourceTree = "";
102 | };
103 | B9BFF1C924D777BC005C9C21 /* Shared */ = {
104 | isa = PBXGroup;
105 | children = (
106 | B9BFF1CA24D777BC005C9C21 /* AdvancedListExampleApp.swift */,
107 | CEAD564622D75C4500FB141B /* ExampleDataProvider.swift */,
108 | B9BFF1CC24D777BD005C9C21 /* Assets.xcassets */,
109 | B9BFF1E824D77866005C9C21 /* Models */,
110 | B9D9E00A22CA8E4800051FE2 /* Views */,
111 | );
112 | path = Shared;
113 | sourceTree = "";
114 | };
115 | B9BFF1D224D777BD005C9C21 /* iOS */ = {
116 | isa = PBXGroup;
117 | children = (
118 | B9BFF1D324D777BD005C9C21 /* Info.plist */,
119 | );
120 | path = iOS;
121 | sourceTree = "";
122 | };
123 | B9BFF1D924D777BD005C9C21 /* macOS */ = {
124 | isa = PBXGroup;
125 | children = (
126 | B9BFF1DA24D777BD005C9C21 /* Info.plist */,
127 | B9BFF1DB24D777BD005C9C21 /* macOS.entitlements */,
128 | );
129 | path = macOS;
130 | sourceTree = "";
131 | };
132 | B9BFF1E824D77866005C9C21 /* Models */ = {
133 | isa = PBXGroup;
134 | children = (
135 | B9884A4A22D3C99000063925 /* ExampleError.swift */,
136 | CE2984AC22D91058002407EF /* AdListItem */,
137 | CE2984AB22D9104B002407EF /* ContactListItem */,
138 | );
139 | path = Models;
140 | sourceTree = "";
141 | };
142 | B9D9DFDB22CA8CDC00051FE2 = {
143 | isa = PBXGroup;
144 | children = (
145 | B99D5FA127A70FDB000F3688 /* AdvancedList */,
146 | B9B7E17623049FD60018BD33 /* README.md */,
147 | B9D9E01222CA971D00051FE2 /* AdvancedList.gif */,
148 | B9BFF1C924D777BC005C9C21 /* Shared */,
149 | B9BFF1D224D777BD005C9C21 /* iOS */,
150 | B9BFF1D924D777BD005C9C21 /* macOS */,
151 | B9D9DFE522CA8CDC00051FE2 /* Products */,
152 | B9B7E173230440850018BD33 /* Frameworks */,
153 | );
154 | sourceTree = "";
155 | };
156 | B9D9DFE522CA8CDC00051FE2 /* Products */ = {
157 | isa = PBXGroup;
158 | children = (
159 | B9BFF1D124D777BD005C9C21 /* AdvancedListExample.app */,
160 | B9BFF1D824D777BD005C9C21 /* AdvancedListExample.app */,
161 | );
162 | name = Products;
163 | sourceTree = "";
164 | };
165 | B9D9E00A22CA8E4800051FE2 /* Views */ = {
166 | isa = PBXGroup;
167 | children = (
168 | B9B5A11A27F4EF74002FD9A0 /* ContentExampleView.swift */,
169 | CEAD564822D7C26300FB141B /* CustomListStateSegmentedControlView.swift */,
170 | B9D9DFEB22CA8CDC00051FE2 /* DataExampleView.swift */,
171 | B9FDFA812803113C0079466B /* ListContentExampleView.swift */,
172 | );
173 | path = Views;
174 | sourceTree = "";
175 | };
176 | CE2984AB22D9104B002407EF /* ContactListItem */ = {
177 | isa = PBXGroup;
178 | children = (
179 | B9884A4E22DA153900063925 /* ContactDetailView.swift */,
180 | B9D9E00B22CA8E5C00051FE2 /* ContactListItem.swift */,
181 | B9884A5022DA167E00063925 /* ContactView.swift */,
182 | CE2984AD22D91066002407EF /* ContactListItemViewRepresentationType.swift */,
183 | B9DA7AA223A2D7F9003C615B /* ContactListItemView.swift */,
184 | );
185 | path = ContactListItem;
186 | sourceTree = "";
187 | };
188 | CE2984AC22D91058002407EF /* AdListItem */ = {
189 | isa = PBXGroup;
190 | children = (
191 | CE2984A922D90FC3002407EF /* AdDetailView.swift */,
192 | CEA92D2422D6127C000C3F7A /* AdListItem.swift */,
193 | B9DA7AA423A2D8E4003C615B /* AdListItemView.swift */,
194 | CE2984AF22D91090002407EF /* AdListItemViewRepresentationType.swift */,
195 | );
196 | path = AdListItem;
197 | sourceTree = "";
198 | };
199 | /* End PBXGroup section */
200 |
201 | /* Begin PBXNativeTarget section */
202 | B9BFF1D024D777BD005C9C21 /* AdvancedListExample (iOS) */ = {
203 | isa = PBXNativeTarget;
204 | buildConfigurationList = B9BFF1E224D777BD005C9C21 /* Build configuration list for PBXNativeTarget "AdvancedListExample (iOS)" */;
205 | buildPhases = (
206 | B9BFF1CD24D777BD005C9C21 /* Sources */,
207 | B9BFF1CE24D777BD005C9C21 /* Frameworks */,
208 | B9BFF1CF24D777BD005C9C21 /* Resources */,
209 | );
210 | buildRules = (
211 | );
212 | dependencies = (
213 | );
214 | name = "AdvancedListExample (iOS)";
215 | packageProductDependencies = (
216 | B9BFF20524D778F5005C9C21 /* AdvancedList */,
217 | );
218 | productName = "AdvancedListExample (iOS)";
219 | productReference = B9BFF1D124D777BD005C9C21 /* AdvancedListExample.app */;
220 | productType = "com.apple.product-type.application";
221 | };
222 | B9BFF1D724D777BD005C9C21 /* AdvancedListExample (macOS) */ = {
223 | isa = PBXNativeTarget;
224 | buildConfigurationList = B9BFF1E524D777BD005C9C21 /* Build configuration list for PBXNativeTarget "AdvancedListExample (macOS)" */;
225 | buildPhases = (
226 | B9BFF1D424D777BD005C9C21 /* Sources */,
227 | B9BFF1D524D777BD005C9C21 /* Frameworks */,
228 | B9BFF1D624D777BD005C9C21 /* Resources */,
229 | );
230 | buildRules = (
231 | );
232 | dependencies = (
233 | );
234 | name = "AdvancedListExample (macOS)";
235 | packageProductDependencies = (
236 | B9BFF20724D77C28005C9C21 /* AdvancedList */,
237 | );
238 | productName = "AdvancedListExample (macOS)";
239 | productReference = B9BFF1D824D777BD005C9C21 /* AdvancedListExample.app */;
240 | productType = "com.apple.product-type.application";
241 | };
242 | /* End PBXNativeTarget section */
243 |
244 | /* Begin PBXProject section */
245 | B9D9DFDC22CA8CDC00051FE2 /* Project object */ = {
246 | isa = PBXProject;
247 | attributes = {
248 | LastSwiftUpdateCheck = 1200;
249 | LastUpgradeCheck = 1100;
250 | ORGANIZATIONNAME = "Christian Elies";
251 | TargetAttributes = {
252 | B9BFF1D024D777BD005C9C21 = {
253 | CreatedOnToolsVersion = 12.0;
254 | };
255 | B9BFF1D724D777BD005C9C21 = {
256 | CreatedOnToolsVersion = 12.0;
257 | };
258 | };
259 | };
260 | buildConfigurationList = B9D9DFDF22CA8CDC00051FE2 /* Build configuration list for PBXProject "AdvancedList-Example" */;
261 | compatibilityVersion = "Xcode 9.3";
262 | developmentRegion = en;
263 | hasScannedForEncodings = 0;
264 | knownRegions = (
265 | en,
266 | Base,
267 | );
268 | mainGroup = B9D9DFDB22CA8CDC00051FE2;
269 | packageReferences = (
270 | B9B7E1782304AE8B0018BD33 /* XCRemoteSwiftPackageReference "AdvancedList" */,
271 | );
272 | productRefGroup = B9D9DFE522CA8CDC00051FE2 /* Products */;
273 | projectDirPath = "";
274 | projectRoot = "";
275 | targets = (
276 | B9BFF1D024D777BD005C9C21 /* AdvancedListExample (iOS) */,
277 | B9BFF1D724D777BD005C9C21 /* AdvancedListExample (macOS) */,
278 | );
279 | };
280 | /* End PBXProject section */
281 |
282 | /* Begin PBXResourcesBuildPhase section */
283 | B9BFF1CF24D777BD005C9C21 /* Resources */ = {
284 | isa = PBXResourcesBuildPhase;
285 | buildActionMask = 2147483647;
286 | files = (
287 | B9BFF1E024D777BD005C9C21 /* Assets.xcassets in Resources */,
288 | );
289 | runOnlyForDeploymentPostprocessing = 0;
290 | };
291 | B9BFF1D624D777BD005C9C21 /* Resources */ = {
292 | isa = PBXResourcesBuildPhase;
293 | buildActionMask = 2147483647;
294 | files = (
295 | B9BFF1E124D777BD005C9C21 /* Assets.xcassets in Resources */,
296 | );
297 | runOnlyForDeploymentPostprocessing = 0;
298 | };
299 | /* End PBXResourcesBuildPhase section */
300 |
301 | /* Begin PBXSourcesBuildPhase section */
302 | B9BFF1CD24D777BD005C9C21 /* Sources */ = {
303 | isa = PBXSourcesBuildPhase;
304 | buildActionMask = 2147483647;
305 | files = (
306 | B9BFF1FB24D778D5005C9C21 /* ContactDetailView.swift in Sources */,
307 | B9BFF1FA24D778D5005C9C21 /* ContactListItemViewRepresentationType.swift in Sources */,
308 | B9BFF1F424D778D1005C9C21 /* AdListItemViewRepresentationType.swift in Sources */,
309 | B9BFF1F324D778D1005C9C21 /* AdDetailView.swift in Sources */,
310 | B9BFF1F924D778D5005C9C21 /* ContactView.swift in Sources */,
311 | B9FDFA822803113C0079466B /* ListContentExampleView.swift in Sources */,
312 | B9BFF1DC24D777BD005C9C21 /* AdvancedListExampleApp.swift in Sources */,
313 | B9BFF1EF24D778CD005C9C21 /* ExampleError.swift in Sources */,
314 | B9BFF1F224D778D1005C9C21 /* AdListItem.swift in Sources */,
315 | B9BFF1E924D7788B005C9C21 /* ExampleDataProvider.swift in Sources */,
316 | B9BFF1F124D778D1005C9C21 /* AdListItemView.swift in Sources */,
317 | B9BFF1EB24D778A2005C9C21 /* DataExampleView.swift in Sources */,
318 | B9B5A11B27F4EF74002FD9A0 /* ContentExampleView.swift in Sources */,
319 | B9BFF20324D778DA005C9C21 /* CustomListStateSegmentedControlView.swift in Sources */,
320 | B9BFF1FD24D778D5005C9C21 /* ContactListItemView.swift in Sources */,
321 | B9BFF1FC24D778D5005C9C21 /* ContactListItem.swift in Sources */,
322 | );
323 | runOnlyForDeploymentPostprocessing = 0;
324 | };
325 | B9BFF1D424D777BD005C9C21 /* Sources */ = {
326 | isa = PBXSourcesBuildPhase;
327 | buildActionMask = 2147483647;
328 | files = (
329 | B9BFF20024D778D5005C9C21 /* ContactDetailView.swift in Sources */,
330 | B9BFF1FF24D778D5005C9C21 /* ContactListItemViewRepresentationType.swift in Sources */,
331 | B9BFF1F824D778D1005C9C21 /* AdListItemViewRepresentationType.swift in Sources */,
332 | B9BFF1F724D778D1005C9C21 /* AdDetailView.swift in Sources */,
333 | B9BFF1FE24D778D5005C9C21 /* ContactView.swift in Sources */,
334 | B9FDFA832803113C0079466B /* ListContentExampleView.swift in Sources */,
335 | B9BFF1DD24D777BD005C9C21 /* AdvancedListExampleApp.swift in Sources */,
336 | B9BFF1F024D778CD005C9C21 /* ExampleError.swift in Sources */,
337 | B9BFF1F624D778D1005C9C21 /* AdListItem.swift in Sources */,
338 | B9BFF1EA24D7788B005C9C21 /* ExampleDataProvider.swift in Sources */,
339 | B9BFF1F524D778D1005C9C21 /* AdListItemView.swift in Sources */,
340 | B9BFF1EC24D778A2005C9C21 /* DataExampleView.swift in Sources */,
341 | B9B5A11C27F4EF74002FD9A0 /* ContentExampleView.swift in Sources */,
342 | B9BFF20424D778DA005C9C21 /* CustomListStateSegmentedControlView.swift in Sources */,
343 | B9BFF20224D778D5005C9C21 /* ContactListItemView.swift in Sources */,
344 | B9BFF20124D778D5005C9C21 /* ContactListItem.swift in Sources */,
345 | );
346 | runOnlyForDeploymentPostprocessing = 0;
347 | };
348 | /* End PBXSourcesBuildPhase section */
349 |
350 | /* Begin XCBuildConfiguration section */
351 | B9BFF1E324D777BD005C9C21 /* Debug */ = {
352 | isa = XCBuildConfiguration;
353 | buildSettings = {
354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
355 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
357 | CODE_SIGN_IDENTITY = "iPhone Developer";
358 | CODE_SIGN_STYLE = Manual;
359 | DEVELOPMENT_TEAM = 766K8ALVVD;
360 | ENABLE_PREVIEWS = YES;
361 | INFOPLIST_FILE = iOS/Info.plist;
362 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
363 | LD_RUNPATH_SEARCH_PATHS = (
364 | "$(inherited)",
365 | "@executable_path/Frameworks",
366 | );
367 | PRODUCT_BUNDLE_IDENTIFIER = de.crelies.AdvancedListExample;
368 | PRODUCT_NAME = AdvancedListExample;
369 | PROVISIONING_PROFILE_SPECIFIER = "match Development de.crelies.*";
370 | SDKROOT = iphoneos;
371 | SWIFT_VERSION = 5.0;
372 | TARGETED_DEVICE_FAMILY = "1,2";
373 | };
374 | name = Debug;
375 | };
376 | B9BFF1E424D777BD005C9C21 /* Release */ = {
377 | isa = XCBuildConfiguration;
378 | buildSettings = {
379 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
380 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
382 | CODE_SIGN_IDENTITY = "iPhone Distribution";
383 | CODE_SIGN_STYLE = Manual;
384 | DEVELOPMENT_TEAM = 766K8ALVVD;
385 | ENABLE_PREVIEWS = YES;
386 | INFOPLIST_FILE = iOS/Info.plist;
387 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
388 | LD_RUNPATH_SEARCH_PATHS = (
389 | "$(inherited)",
390 | "@executable_path/Frameworks",
391 | );
392 | PRODUCT_BUNDLE_IDENTIFIER = de.crelies.AdvancedListExample;
393 | PRODUCT_NAME = AdvancedListExample;
394 | PROVISIONING_PROFILE_SPECIFIER = "match AdHoc de.crelies.*";
395 | SDKROOT = iphoneos;
396 | SWIFT_VERSION = 5.0;
397 | TARGETED_DEVICE_FAMILY = "1,2";
398 | VALIDATE_PRODUCT = YES;
399 | };
400 | name = Release;
401 | };
402 | B9BFF1E624D777BD005C9C21 /* Debug */ = {
403 | isa = XCBuildConfiguration;
404 | buildSettings = {
405 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
406 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
407 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
408 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
409 | CODE_SIGN_IDENTITY = "-";
410 | CODE_SIGN_STYLE = Manual;
411 | COMBINE_HIDPI_IMAGES = YES;
412 | DEVELOPMENT_TEAM = 766K8ALVVD;
413 | ENABLE_PREVIEWS = YES;
414 | INFOPLIST_FILE = macOS/Info.plist;
415 | LD_RUNPATH_SEARCH_PATHS = (
416 | "$(inherited)",
417 | "@executable_path/../Frameworks",
418 | );
419 | MACOSX_DEPLOYMENT_TARGET = 12.0;
420 | PRODUCT_BUNDLE_IDENTIFIER = de.crelies.AdvancedListExample;
421 | PRODUCT_NAME = AdvancedListExample;
422 | PROVISIONING_PROFILE_SPECIFIER = "";
423 | SDKROOT = macosx;
424 | SWIFT_VERSION = 5.0;
425 | };
426 | name = Debug;
427 | };
428 | B9BFF1E724D777BD005C9C21 /* Release */ = {
429 | isa = XCBuildConfiguration;
430 | buildSettings = {
431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
432 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
433 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
434 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
435 | CODE_SIGN_IDENTITY = "-";
436 | CODE_SIGN_STYLE = Manual;
437 | COMBINE_HIDPI_IMAGES = YES;
438 | DEVELOPMENT_TEAM = 766K8ALVVD;
439 | ENABLE_PREVIEWS = YES;
440 | INFOPLIST_FILE = macOS/Info.plist;
441 | LD_RUNPATH_SEARCH_PATHS = (
442 | "$(inherited)",
443 | "@executable_path/../Frameworks",
444 | );
445 | MACOSX_DEPLOYMENT_TARGET = 12.0;
446 | PRODUCT_BUNDLE_IDENTIFIER = de.crelies.AdvancedListExample;
447 | PRODUCT_NAME = AdvancedListExample;
448 | PROVISIONING_PROFILE_SPECIFIER = "";
449 | SDKROOT = macosx;
450 | SWIFT_VERSION = 5.0;
451 | };
452 | name = Release;
453 | };
454 | B9D9DFF622CA8CDD00051FE2 /* Debug */ = {
455 | isa = XCBuildConfiguration;
456 | buildSettings = {
457 | ALWAYS_SEARCH_USER_PATHS = NO;
458 | CLANG_ANALYZER_NONNULL = YES;
459 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
460 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
461 | CLANG_CXX_LIBRARY = "libc++";
462 | CLANG_ENABLE_MODULES = YES;
463 | CLANG_ENABLE_OBJC_ARC = YES;
464 | CLANG_ENABLE_OBJC_WEAK = YES;
465 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
466 | CLANG_WARN_BOOL_CONVERSION = YES;
467 | CLANG_WARN_COMMA = YES;
468 | CLANG_WARN_CONSTANT_CONVERSION = YES;
469 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
470 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
471 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
472 | CLANG_WARN_EMPTY_BODY = YES;
473 | CLANG_WARN_ENUM_CONVERSION = YES;
474 | CLANG_WARN_INFINITE_RECURSION = YES;
475 | CLANG_WARN_INT_CONVERSION = YES;
476 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
477 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
478 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
479 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
480 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
481 | CLANG_WARN_STRICT_PROTOTYPES = YES;
482 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
483 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
484 | CLANG_WARN_UNREACHABLE_CODE = YES;
485 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
486 | COPY_PHASE_STRIP = NO;
487 | DEBUG_INFORMATION_FORMAT = dwarf;
488 | ENABLE_STRICT_OBJC_MSGSEND = YES;
489 | ENABLE_TESTABILITY = YES;
490 | GCC_C_LANGUAGE_STANDARD = gnu11;
491 | GCC_DYNAMIC_NO_PIC = NO;
492 | GCC_NO_COMMON_BLOCKS = YES;
493 | GCC_OPTIMIZATION_LEVEL = 0;
494 | GCC_PREPROCESSOR_DEFINITIONS = (
495 | "DEBUG=1",
496 | "$(inherited)",
497 | );
498 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
499 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
500 | GCC_WARN_UNDECLARED_SELECTOR = YES;
501 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
502 | GCC_WARN_UNUSED_FUNCTION = YES;
503 | GCC_WARN_UNUSED_VARIABLE = YES;
504 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
505 | MACOSX_DEPLOYMENT_TARGET = 12.0;
506 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
507 | MTL_FAST_MATH = YES;
508 | ONLY_ACTIVE_ARCH = YES;
509 | SDKROOT = iphoneos;
510 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
511 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
512 | };
513 | name = Debug;
514 | };
515 | B9D9DFF722CA8CDD00051FE2 /* Release */ = {
516 | isa = XCBuildConfiguration;
517 | buildSettings = {
518 | ALWAYS_SEARCH_USER_PATHS = NO;
519 | CLANG_ANALYZER_NONNULL = YES;
520 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
521 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
522 | CLANG_CXX_LIBRARY = "libc++";
523 | CLANG_ENABLE_MODULES = YES;
524 | CLANG_ENABLE_OBJC_ARC = YES;
525 | CLANG_ENABLE_OBJC_WEAK = YES;
526 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
527 | CLANG_WARN_BOOL_CONVERSION = YES;
528 | CLANG_WARN_COMMA = YES;
529 | CLANG_WARN_CONSTANT_CONVERSION = YES;
530 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
531 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
532 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
533 | CLANG_WARN_EMPTY_BODY = YES;
534 | CLANG_WARN_ENUM_CONVERSION = YES;
535 | CLANG_WARN_INFINITE_RECURSION = YES;
536 | CLANG_WARN_INT_CONVERSION = YES;
537 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
538 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
539 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
540 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
541 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
542 | CLANG_WARN_STRICT_PROTOTYPES = YES;
543 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
544 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
545 | CLANG_WARN_UNREACHABLE_CODE = YES;
546 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
547 | COPY_PHASE_STRIP = NO;
548 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
549 | ENABLE_NS_ASSERTIONS = NO;
550 | ENABLE_STRICT_OBJC_MSGSEND = YES;
551 | GCC_C_LANGUAGE_STANDARD = gnu11;
552 | GCC_NO_COMMON_BLOCKS = YES;
553 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
554 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
555 | GCC_WARN_UNDECLARED_SELECTOR = YES;
556 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
557 | GCC_WARN_UNUSED_FUNCTION = YES;
558 | GCC_WARN_UNUSED_VARIABLE = YES;
559 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
560 | MACOSX_DEPLOYMENT_TARGET = 12.0;
561 | MTL_ENABLE_DEBUG_INFO = NO;
562 | MTL_FAST_MATH = YES;
563 | SDKROOT = iphoneos;
564 | SWIFT_COMPILATION_MODE = wholemodule;
565 | SWIFT_OPTIMIZATION_LEVEL = "-O";
566 | VALIDATE_PRODUCT = YES;
567 | };
568 | name = Release;
569 | };
570 | /* End XCBuildConfiguration section */
571 |
572 | /* Begin XCConfigurationList section */
573 | B9BFF1E224D777BD005C9C21 /* Build configuration list for PBXNativeTarget "AdvancedListExample (iOS)" */ = {
574 | isa = XCConfigurationList;
575 | buildConfigurations = (
576 | B9BFF1E324D777BD005C9C21 /* Debug */,
577 | B9BFF1E424D777BD005C9C21 /* Release */,
578 | );
579 | defaultConfigurationIsVisible = 0;
580 | defaultConfigurationName = Release;
581 | };
582 | B9BFF1E524D777BD005C9C21 /* Build configuration list for PBXNativeTarget "AdvancedListExample (macOS)" */ = {
583 | isa = XCConfigurationList;
584 | buildConfigurations = (
585 | B9BFF1E624D777BD005C9C21 /* Debug */,
586 | B9BFF1E724D777BD005C9C21 /* Release */,
587 | );
588 | defaultConfigurationIsVisible = 0;
589 | defaultConfigurationName = Release;
590 | };
591 | B9D9DFDF22CA8CDC00051FE2 /* Build configuration list for PBXProject "AdvancedList-Example" */ = {
592 | isa = XCConfigurationList;
593 | buildConfigurations = (
594 | B9D9DFF622CA8CDD00051FE2 /* Debug */,
595 | B9D9DFF722CA8CDD00051FE2 /* Release */,
596 | );
597 | defaultConfigurationIsVisible = 0;
598 | defaultConfigurationName = Release;
599 | };
600 | /* End XCConfigurationList section */
601 |
602 | /* Begin XCRemoteSwiftPackageReference section */
603 | B9B7E1782304AE8B0018BD33 /* XCRemoteSwiftPackageReference "AdvancedList" */ = {
604 | isa = XCRemoteSwiftPackageReference;
605 | repositoryURL = "https://github.com/crelies/AdvancedList";
606 | requirement = {
607 | kind = upToNextMajorVersion;
608 | minimumVersion = 6.0.0;
609 | };
610 | };
611 | /* End XCRemoteSwiftPackageReference section */
612 |
613 | /* Begin XCSwiftPackageProductDependency section */
614 | B9BFF20524D778F5005C9C21 /* AdvancedList */ = {
615 | isa = XCSwiftPackageProductDependency;
616 | package = B9B7E1782304AE8B0018BD33 /* XCRemoteSwiftPackageReference "AdvancedList" */;
617 | productName = AdvancedList;
618 | };
619 | B9BFF20724D77C28005C9C21 /* AdvancedList */ = {
620 | isa = XCSwiftPackageProductDependency;
621 | package = B9B7E1782304AE8B0018BD33 /* XCRemoteSwiftPackageReference "AdvancedList" */;
622 | productName = AdvancedList;
623 | };
624 | /* End XCSwiftPackageProductDependency section */
625 | };
626 | rootObject = B9D9DFDC22CA8CDC00051FE2 /* Project object */;
627 | }
628 |
--------------------------------------------------------------------------------
/Example/AdvancedList-Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/AdvancedList-Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/AdvancedList-Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "ListPagination",
6 | "repositoryURL": "https://github.com/crelies/ListPagination",
7 | "state": {
8 | "branch": null,
9 | "revision": "af667b368a10512da79605b61698d53c11d24cbf",
10 | "version": "0.1.1"
11 | }
12 | },
13 | {
14 | "package": "ViewInspector",
15 | "repositoryURL": "https://github.com/nalexn/ViewInspector.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "6b88c4ec1fa20cf38f2138052e63c8e79df5d76e",
19 | "version": "0.9.1"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Example/AdvancedList-Example.xcodeproj/xcshareddata/xcschemes/AdvancedListExample (iOS).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 |
--------------------------------------------------------------------------------
/Example/AdvancedList.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chris-swift-dev/AdvancedList/56e0ee9634b7be8d7a5fffb35aefa702f7ad20a8/Example/AdvancedList.gif
--------------------------------------------------------------------------------
/Example/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Christian Elies
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 |
--------------------------------------------------------------------------------
/Example/README.md:
--------------------------------------------------------------------------------
1 | # AdvancedList-SwiftUI
2 |
3 | This repository contains a usage example of my Swift package for an advanced **SwiftUI** `List view` which adds **pagination** and an **empty, error and loading state** on top of the existing `List view`. You can find the Swift package [here](https://github.com/crelies/AdvancedList).
4 |
5 | ## Motivation
6 |
7 | I often deal with lists which can have different view states (empty, error, items or loading). Currently I use an extended version of **IGListKit** to create these advanced lists in my UIKit applications.
8 |
9 | ## Preview
10 |
11 | 
12 |
--------------------------------------------------------------------------------
/Example/Shared/AdvancedListExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListExampleApp.swift
3 | // Shared
4 | //
5 | // Created by Christian Elies on 03.08.20.
6 | // Copyright © 2020 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | @main
13 | struct AdvancedListExampleApp: App {
14 | private var navigationStyle: some NavigationViewStyle {
15 | #if os(iOS)
16 | return StackNavigationViewStyle()
17 | #else
18 | return DefaultNavigationViewStyle()
19 | #endif
20 | }
21 |
22 | var body: some Scene {
23 | WindowGroup {
24 | NavigationView {
25 | List {
26 | NavigationLink("Data example", destination: DataExampleView())
27 | NavigationLink("List content example", destination: ListContentExampleView())
28 | NavigationLink("Content example", destination: ContentExampleView())
29 | }
30 | .navigationTitle("Examples")
31 | }
32 | .navigationViewStyle(navigationStyle)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/restaurant.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "restaurant.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/Shared/Assets.xcassets/restaurant.imageset/restaurant.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chris-swift-dev/AdvancedList/56e0ee9634b7be8d7a5fffb35aefa702f7ad20a8/Example/Shared/Assets.xcassets/restaurant.imageset/restaurant.jpg
--------------------------------------------------------------------------------
/Example/Shared/ExampleDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleDataProvider.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 11.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | final class ExampleDataProvider {
13 | static func randomItems() -> [AnyIdentifiable] {
14 | let itemCount = Array(5...15).randomElement()!
15 | let items: [AnyIdentifiable] = Array(0...itemCount).map { _ in
16 | if Bool.random() {
17 | let id = UUID().uuidString
18 | let firstName = "Max"
19 | let lastName = "Mustermann"
20 | let streetAddress = "Schlossallee 19"
21 | let zip = "20097"
22 | let city = "Hamburg"
23 | let viewRepresentationType = ContactListItemViewRepresentationType.allCases.randomElement()!
24 | let itemModel = ContactListItem(
25 | id: id,
26 | firstName: firstName,
27 | lastName: lastName,
28 | streetAddress: streetAddress,
29 | zip: zip,
30 | city: city,
31 | viewRepresentationType: viewRepresentationType
32 | )
33 | return AnyIdentifiable(itemModel)
34 | } else {
35 | let id = UUID().uuidString
36 | let text = "⚠️ This is a really long and annoying advertisement I want to get rid off. Everyone knows that it's hard to hide from ads. They always find us ☢️!"
37 | let viewRepresentationType = AdListItemViewRepresentationType.allCases.randomElement()!
38 | let itemModel = AdListItem(
39 | id: id,
40 | text: text,
41 | viewRepresentationType: viewRepresentationType
42 | )
43 | return AnyIdentifiable(itemModel)
44 | }
45 | }
46 | return items
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Example/Shared/Models/AdListItem/AdDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdDetailView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 12.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct AdDetailView : View {
12 | let text: String
13 |
14 | var body: some View {
15 | Text(text)
16 | .lineLimit(nil)
17 | .padding()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Example/Shared/Models/AdListItem/AdListItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdListItem.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 01.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct AdListItem: Identifiable, Hashable {
12 | let id: String
13 | let text: String
14 |
15 | var viewRepresentationType: AdListItemViewRepresentationType = .short
16 | }
17 |
--------------------------------------------------------------------------------
/Example/Shared/Models/AdListItem/AdListItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdListItemView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 12.12.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct AdListItemView: View {
12 | @State private var isImageCollapsed: Bool = true
13 |
14 | let adListItem: AdListItem
15 |
16 | var body: some View {
17 | if adListItem.viewRepresentationType == .short {
18 | NavigationLink(destination: AdDetailView(text: adListItem.text), label: {
19 | Text(adListItem.text)
20 | .lineLimit(1)
21 | Text("ℹ️")
22 | })
23 | } else if adListItem.viewRepresentationType == .long {
24 | Text(adListItem.text)
25 | .lineLimit(nil)
26 | } else if adListItem.viewRepresentationType == .image {
27 | VStack {
28 | if !isImageCollapsed {
29 | Image("restaurant")
30 | .resizable()
31 | .aspectRatio(contentMode: .fit)
32 | .frame(height: 200)
33 | }
34 |
35 | Button(action: {
36 | self.isImageCollapsed.toggle()
37 | }) {
38 | Text("\(isImageCollapsed ? "show" : "hide") image")
39 | }.foregroundColor(.blue)
40 | }
41 | }
42 | }
43 | }
44 |
45 | #if DEBUG
46 | struct AdListItemView_Previews: PreviewProvider {
47 | static var previews: some View {
48 | AdListItemView(adListItem: AdListItem(id: "ID", text: "Text"))
49 | }
50 | }
51 | #endif
52 |
--------------------------------------------------------------------------------
/Example/Shared/Models/AdListItem/AdListItemViewRepresentationType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdListItemViewRepresentationType.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 12.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | enum AdListItemViewRepresentationType: CaseIterable {
10 | case short
11 | case long
12 | case image
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ContactListItem/ContactDetailView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactDetailView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 13.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContactDetailView : View {
12 | let listItem: ContactListItem
13 |
14 | var body: some View {
15 | VStack {
16 | HStack {
17 | Text("🕵️")
18 | Text(listItem.firstName)
19 | Text(listItem.lastName)
20 | }
21 | Text(listItem.streetAddress)
22 | HStack {
23 | Text(listItem.zip)
24 | Text(listItem.city)
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ContactListItem/ContactListItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactListItem.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 01.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContactListItem: Identifiable, Hashable {
12 | let id: String
13 | let firstName: String
14 | let lastName: String
15 | let streetAddress: String
16 | let zip: String
17 | let city: String
18 |
19 | var viewRepresentationType: ContactListItemViewRepresentationType = .short
20 | }
21 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ContactListItem/ContactListItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactListItemView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 12.12.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContactListItemView: View {
12 | @State private var collapsed: Bool = true
13 |
14 | let contactListItem: ContactListItem
15 |
16 | var body: some View {
17 | if contactListItem.viewRepresentationType == .short {
18 | ContactView(firstName: contactListItem.firstName,
19 | lastName: contactListItem.lastName,
20 | hasMoreInformation: false)
21 | } else if contactListItem.viewRepresentationType == .detail {
22 | NavigationLink(destination: ContactDetailView(listItem: contactListItem), label: {
23 | ContactView(firstName: contactListItem.firstName,
24 | lastName: contactListItem.lastName,
25 | hasMoreInformation: true)
26 | })
27 | } else if contactListItem.viewRepresentationType == .collapsable {
28 | VStack {
29 | if collapsed {
30 | ContactView(firstName: contactListItem.firstName,
31 | lastName: contactListItem.lastName,
32 | hasMoreInformation: false)
33 | } else {
34 | ContactDetailView(listItem: contactListItem)
35 | }
36 |
37 | Button(action: {
38 | self.collapsed.toggle()
39 | }) {
40 | Text("\(collapsed ? "show" : "hide") details")
41 | }.foregroundColor(.blue)
42 | }
43 | }
44 | }
45 | }
46 |
47 | #if DEBUG
48 | struct ContactListOverviewView_Previews: PreviewProvider {
49 | static var previews: some View {
50 | ContactListItemView(contactListItem: ContactListItem(id: "ID",
51 | firstName: "Max",
52 | lastName: "Example",
53 | streetAddress: "Awesome street 5",
54 | zip: "20097",
55 | city: "Hamburg"))
56 | }
57 | }
58 | #endif
59 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ContactListItem/ContactListItemViewRepresentationType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactListItemViewRepresentationType.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 12.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | enum ContactListItemViewRepresentationType: CaseIterable {
10 | case short
11 | case detail
12 | case collapsable
13 | }
14 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ContactListItem/ContactView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContactView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 13.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContactView : View {
12 | let firstName: String
13 | let lastName: String
14 | let hasMoreInformation: Bool
15 |
16 | var body: some View {
17 | HStack {
18 | Text("🕵️")
19 | Text(firstName)
20 | Text(lastName)
21 | if hasMoreInformation {
22 | Spacer()
23 | Text("ℹ️")
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Example/Shared/Models/ExampleError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExampleError.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 08.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum ExampleError: Error, CaseIterable {
12 | case requestTimedOut
13 | case unknown
14 | }
15 |
16 | extension ExampleError: LocalizedError {
17 | var errorDescription: String? {
18 | switch self {
19 | case .requestTimedOut:
20 | return "The request timed out."
21 | case .unknown:
22 | return "An unknown error occurred."
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Example/Shared/Views/ContentExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentExampleView.swift
3 | // AdvancedList-Example
4 | //
5 | // Created by Christian Elies on 30/03/2022.
6 | // Copyright © 2022 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | struct ContentExampleView: View {
13 | @State private var listState: ListState = .items
14 |
15 | var body: some View {
16 | VStack(spacing: 16) {
17 | CustomListStateSegmentedControlView(
18 | items: .constant([]),
19 | listState: $listState,
20 | paginationState: .constant(.idle),
21 | shouldHideEmptyOption: true
22 | )
23 |
24 | AdvancedList(listState: listState, content: {
25 | VStack {
26 | Text("Example 1")
27 | Text("Example 2")
28 | Text("Example 3")
29 | }
30 | .frame(maxHeight: .infinity)
31 | }, errorStateView: { error in
32 | VStack(alignment: .leading) {
33 | Text("Error").foregroundColor(.primary)
34 | Text(error.localizedDescription).foregroundColor(.secondary)
35 | }
36 | .frame(maxHeight: .infinity)
37 | }, loadingStateView: {
38 | VStack {
39 | ProgressView()
40 | }
41 | .frame(maxHeight: .infinity)
42 | })
43 |
44 | Spacer()
45 | }
46 | .navigationTitle("Content example")
47 | }
48 | }
49 |
50 | #if DEBUG
51 | struct ContentExampleView_Previews: PreviewProvider {
52 | static var previews: some View {
53 | ContentExampleView()
54 | }
55 | }
56 | #endif
57 |
--------------------------------------------------------------------------------
/Example/Shared/Views/CustomListStateSegmentedControlView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomListStateSegmentedControlView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 11.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | struct CustomListStateSegmentedControlView : View {
13 | @Binding var items: [AnyIdentifiable]
14 | @Binding var listState: ListState
15 | @Binding var paginationState: AdvancedListPaginationState
16 | var shouldHideEmptyOption = false
17 |
18 | var body: some View {
19 | HStack {
20 | Button(action: {
21 | let items = ExampleDataProvider.randomItems()
22 | self.items.removeAll()
23 | self.items.append(contentsOf: items)
24 |
25 | listState = .items
26 | paginationState = .idle
27 | }) {
28 | Text("Items")
29 | }
30 |
31 | if !shouldHideEmptyOption {
32 | Button(action: {
33 | items.removeAll()
34 |
35 | listState = .items
36 | paginationState = .idle
37 | }) {
38 | Text("Empty")
39 | }
40 | }
41 |
42 | Button(action: {
43 | listState = .loading
44 | paginationState = .idle
45 | }) {
46 | Text("Loading")
47 | }
48 |
49 | Button(action: {
50 | listState = .error(ExampleError.allCases.randomElement()! as NSError)
51 | paginationState = .idle
52 | }) {
53 | Text("Error")
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Example/Shared/Views/DataExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataExampleView.swift
3 | // AdvancedListExample
4 | //
5 | // Created by Christian Elies on 01.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | struct DataExampleView: View {
13 | @State private var items = ExampleDataProvider.randomItems()
14 | @State private var listState: ListState = .items
15 | @State private var paginationState: AdvancedListPaginationState = .idle
16 |
17 | private var backgroundColor: Color? {
18 | #if os(iOS)
19 | return Color(.secondarySystemBackground)
20 | #else
21 | return nil
22 | #endif
23 | }
24 |
25 | var body: some View {
26 | VStack(spacing: 16) {
27 | CustomListStateSegmentedControlView(
28 | items: $items,
29 | listState: $listState,
30 | paginationState: $paginationState
31 | )
32 |
33 | list()
34 |
35 | Spacer()
36 | }
37 | .navigationTitle("Data example")
38 | }
39 | }
40 |
41 | private extension DataExampleView {
42 | @ViewBuilder
43 | func list() -> some View {
44 | let advancedList = AdvancedList(items, content: { item in
45 | view(for: item)
46 | }, listState: listState, emptyStateView: {
47 | VStack {
48 | Text("No data")
49 | }
50 | .frame(maxHeight: .infinity)
51 | }, errorStateView: { error in
52 | VStack {
53 | Text("\(error.localizedDescription)").lineLimit(nil)
54 | }
55 | .frame(maxHeight: .infinity)
56 | }, loadingStateView: {
57 | VStack {
58 | ProgressView()
59 | }
60 | .frame(maxHeight: .infinity)
61 | })
62 | .pagination(.init(type: .lastItem, shouldLoadNextPage: loadNextItems) {
63 | switch paginationState {
64 | case .idle:
65 | EmptyView()
66 | case .loading:
67 | paginationLoadingStateView()
68 | case let .error(error):
69 | paginationErrorStateView(error)
70 | }
71 | })
72 |
73 | if #available(tvOS 15, *) {
74 | advancedList
75 | .refreshable(action: refresh)
76 | } else {
77 | advancedList
78 | }
79 | }
80 |
81 | func paginationLoadingStateView() -> some View {
82 | HStack {
83 | Spacer()
84 | ProgressView()
85 | Spacer()
86 | }
87 | .padding()
88 | .background(backgroundColor)
89 | .cornerRadius(16)
90 | .padding(.horizontal)
91 | }
92 |
93 | func paginationErrorStateView(_ error: Error) -> some View {
94 | HStack {
95 | Spacer()
96 | VStack(spacing: 8) {
97 | Text(error.localizedDescription)
98 | .foregroundColor(.red)
99 | .lineLimit(nil)
100 | .multilineTextAlignment(.center)
101 |
102 | Button(action: {
103 | loadNextItems()
104 | }) {
105 | Text("Retry")
106 | }
107 | }
108 | Spacer()
109 | }
110 | .padding()
111 | .background(backgroundColor)
112 | .cornerRadius(16)
113 | .padding(.horizontal)
114 | }
115 |
116 | @Sendable
117 | func refresh() async {
118 | listState = .loading
119 |
120 | Task(priority: .userInitiated) {
121 | let duration = UInt64(1.5 * 1_000_000_000)
122 | try? await Task.sleep(nanoseconds: duration)
123 |
124 | let items = ExampleDataProvider.randomItems()
125 | self.items.removeAll()
126 | self.items.append(contentsOf: items)
127 |
128 | listState = .items
129 | paginationState = .idle
130 | }
131 | }
132 | }
133 |
134 | private extension DataExampleView {
135 | @ViewBuilder func view(for identifiable: AnyIdentifiable) -> some View {
136 | if let contactListItem = identifiable.value as? ContactListItem {
137 | ContactListItemView(contactListItem: contactListItem)
138 | } else if let adListItem = identifiable.value as? AdListItem {
139 | AdListItemView(adListItem: adListItem)
140 | } else {
141 | EmptyView()
142 | }
143 | }
144 |
145 | func loadNextItems() {
146 | guard paginationState != .loading else {
147 | return
148 | }
149 |
150 | paginationState = .loading
151 |
152 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
153 | if Bool.random() {
154 | items.append(contentsOf: ExampleDataProvider.randomItems())
155 | paginationState = .idle
156 | } else {
157 | paginationState = .error(ExampleError.requestTimedOut as NSError)
158 | }
159 | }
160 | }
161 | }
162 |
163 | #if DEBUG
164 | struct ContentView_Previews : PreviewProvider {
165 | static var previews: some View {
166 | DataExampleView()
167 | }
168 | }
169 | #endif
170 |
--------------------------------------------------------------------------------
/Example/Shared/Views/ListContentExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListContentExampleView.swift
3 | // AdvancedList-Example
4 | //
5 | // Created by Christian Elies on 10/04/2022.
6 | // Copyright © 2022 Christian Elies. All rights reserved.
7 | //
8 |
9 | import AdvancedList
10 | import SwiftUI
11 |
12 | struct ListContentExampleView: View {
13 | @State private var listState: ListState = .items
14 |
15 | var body: some View {
16 | VStack(spacing: 16) {
17 | CustomListStateSegmentedControlView(
18 | items: .constant([]),
19 | listState: $listState,
20 | paginationState: .constant(.idle),
21 | shouldHideEmptyOption: true
22 | )
23 |
24 | AdvancedList(listState: listState, listContent: {
25 | Text("Example 1")
26 | Text("Example 2")
27 | Text("Example 3")
28 | }, errorStateView: { error in
29 | VStack(alignment: .leading) {
30 | Text("Error").foregroundColor(.primary)
31 | Text(error.localizedDescription).foregroundColor(.secondary)
32 | }
33 | .frame(maxHeight: .infinity)
34 | }, loadingStateView: {
35 | VStack {
36 | ProgressView()
37 | }
38 | .frame(maxHeight: .infinity)
39 | })
40 |
41 | Spacer()
42 | }
43 | .navigationTitle("List content example")
44 | }
45 | }
46 |
47 | #if DEBUG
48 | struct ListContentExampleView_Previews: PreviewProvider {
49 | static var previews: some View {
50 | ListContentExampleView()
51 | }
52 | }
53 | #endif
54 |
--------------------------------------------------------------------------------
/Example/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 |
28 | UIApplicationSupportsIndirectInputEvents
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Example/macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSHumanReadableCopyright
26 | Copyright © 2020 Christian Elies. All rights reserved.
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Example/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Christian Elies
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": "ListPagination",
6 | "repositoryURL": "https://github.com/crelies/ListPagination",
7 | "state": {
8 | "branch": null,
9 | "revision": "af667b368a10512da79605b61698d53c11d24cbf",
10 | "version": "0.1.1"
11 | }
12 | },
13 | {
14 | "package": "ViewInspector",
15 | "repositoryURL": "https://github.com/nalexn/ViewInspector.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "ec943ed718cd293b95f17a2b81e8917d6ed70752",
19 | "version": "0.3.8"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "AdvancedList",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .tvOS(.v13)
12 | ],
13 | products: [
14 | .library(
15 | name: "AdvancedList",
16 | targets: ["AdvancedList"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/crelies/ListPagination", from: "0.1.0"),
20 | .package(url: "https://github.com/nalexn/ViewInspector.git", from: "0.3.8")
21 | ],
22 | targets: [
23 | .target(
24 | name: "AdvancedList",
25 | dependencies: ["ListPagination"]),
26 | .testTarget(
27 | name: "AdvancedListTests",
28 | dependencies: ["AdvancedList", "ViewInspector"]),
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AdvancedList
2 |
3 | [](https://developer.apple.com/swift)
4 | [](https://www.apple.com)
5 | [](https://github.com/crelies/AdvancedList)
6 | [](https://github.com/crelies/AdvancedList/actions/workflows/build.yml)
7 | [](https://codecov.io/gh/crelies/AdvancedList)
8 | [](https://en.wikipedia.org/wiki/MIT_License)
9 |
10 | This package provides a wrapper view around the **SwiftUI** `List view` which adds **pagination** (through my [ListPagination package](https://github.com/crelies/ListPagination)) and an **empty**, **error** and **loading state** including a corresponding view.
11 |
12 | ## 📦 Installation
13 |
14 | Add this Swift package in Xcode using its Github repository url. (File > Swift Packages > Add Package Dependency...)
15 |
16 | ## 🚀 How to use
17 |
18 | The `AdvancedList` view is similar to the `List` and `ForEach` views. You have to pass data (`RandomAccessCollection`) and a view provider (`(Data.Element) -> some View`) to the initializer. In addition to the `List` view the `AdvancedList` expects a list state and corresponding views.
19 | Modify your data anytime or hide an item through the content block if you like. The view is updated automatically 🎉.
20 |
21 | ```swift
22 | import AdvancedList
23 |
24 | @State private var listState: ListState = .items
25 |
26 | AdvancedList(yourData, content: { item in
27 | Text("Item")
28 | }, listState: listState, emptyStateView: {
29 | Text("No data")
30 | }, errorStateView: { error in
31 | Text(error.localizedDescription)
32 | .lineLimit(nil)
33 | }, loadingStateView: {
34 | Text("Loading ...")
35 | })
36 | ```
37 |
38 | ### 🆕 Custom List view
39 |
40 | Starting from version `6.0.0` you can use a custom list view instead of the `SwiftUI` `List` used under the hood. As an example you can now easily use the **LazyVStack** introduced in **iOS 14** if needed.
41 |
42 | Upgrade from version `5.0.0` **without breaking anything**. Simply add the **listView parameter** after the upgrade:
43 |
44 | ```swift
45 | AdvancedList(yourData, listView: { rows in
46 | if #available(iOS 14, macOS 11, *) {
47 | ScrollView {
48 | LazyVStack(alignment: .leading, content: rows)
49 | .padding()
50 | }
51 | } else {
52 | List(content: rows)
53 | }
54 | }, content: { item in
55 | Text("Item")
56 | }, listState: listState, emptyStateView: {
57 | Text("No data")
58 | }, errorStateView: { error in
59 | Text(error.localizedDescription)
60 | .lineLimit(nil)
61 | }, loadingStateView: {
62 | Text("Loading ...")
63 | })
64 | ```
65 |
66 | ### 🆕 Custom Content view
67 |
68 | Starting from version `8.0.0` you have full freedom & control over the content view rendered in the `items` state of your `AdvancedList`. Use a `SwiftUI List` or a `custom view`.
69 |
70 | Upgrade from version `7.0.0` **without breaking anything** and use the new API:
71 |
72 | ```swift
73 | AdvancedList(listState: yourListState, content: {
74 | VStack {
75 | Text("Row 1")
76 | Text("Row 2")
77 | Text("Row 3")
78 | }
79 | }, errorStateView: { error in
80 | VStack(alignment: .leading) {
81 | Text("Error").foregroundColor(.primary)
82 | Text(error.localizedDescription).foregroundColor(.secondary)
83 | }
84 | }, loadingStateView: ProgressView.init)
85 | ```
86 |
87 | ### 📄 Pagination
88 |
89 | The `Pagination` functionality is now (>= `5.0.0`) implemented as a `modifier`.
90 | It has three different states: `error`, `idle` and `loading`. If the `state` of the `Pagination` changes the `AdvancedList` displays the view created by the view builder of the specified pagination object (`AdvancedListPagination`). Keep track of the current pagination state by creating a local state variable (`@State`) of type `AdvancedListPaginationState`. Use this state variable in the `content` `ViewBuilder` of your pagination configuration object to determine which view should be displayed in the list (see the example below).
91 |
92 | If you want to use pagination you can choose between the `lastItemPagination` and the `thresholdItemPagination`. Both concepts are described [here](https://github.com/crelies/ListPagination). Just specify the type of the pagination when adding the `.pagination` modifier to your `AdvancedList`.
93 |
94 | **The view created by the `content` `ViewBuilder` of your pagination configuration object will only be visible below the List if the last item of the List appeared! That way the user is only interrupted if needed.**
95 |
96 | **Example:**
97 |
98 | ```swift
99 | @State private var paginationState: AdvancedListPaginationState = .idle
100 |
101 | AdvancedList(...)
102 | .pagination(.init(type: .lastItem, shouldLoadNextPage: {
103 | paginationState = .loading
104 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
105 | items.append(contentsOf: moreItems)
106 | paginationState = .idle
107 | }
108 | }) {
109 | switch paginationState {
110 | case .idle:
111 | EmptyView()
112 | case .loading:
113 | if #available(iOS 14, *) {
114 | ProgressView()
115 | } else {
116 | Text("Loading ...")
117 | }
118 | case let .error(error):
119 | Text(error.localizedDescription)
120 | }
121 | })
122 | ```
123 |
124 | ### 📁 Move and 🗑️ delete items
125 |
126 | To enable the move or delete function just use the related `onMove` or `onDelete` view modifier.
127 | **Per default the functions are disabled if you don't add the view modifiers.**
128 |
129 | ```swift
130 | import AdvancedList
131 |
132 | @State private var listState: ListState = .items
133 |
134 | AdvancedList(yourData, content: { item in
135 | Text("Item")
136 | }, listState: listState, emptyStateView: {
137 | Text("No data")
138 | }, errorStateView: { error in
139 | Text(error.localizedDescription)
140 | .lineLimit(nil)
141 | }, loadingStateView: {
142 | Text("Loading ...")
143 | })
144 | .onMove { (indexSet, index) in
145 | // move me
146 | }
147 | .onDelete { indexSet in
148 | // delete me
149 | }
150 | ```
151 |
152 | ### 🎛️ Filtering
153 |
154 | **You can hide items in your list through the content block.** Only return a view in the content block if a specific condition is met.
155 |
156 | ## 🎁 Example
157 |
158 | The following code shows how easy-to-use the view is:
159 |
160 | ```swift
161 | import AdvancedList
162 |
163 | @State private var listState: ListState = .items
164 |
165 | AdvancedList(yourData, content: { item in
166 | Text("Item")
167 | }, listState: listState, emptyStateView: {
168 | Text("No data")
169 | }, errorStateView: { error in
170 | VStack {
171 | Text(error.localizedDescription)
172 | .lineLimit(nil)
173 |
174 | Button(action: {
175 | // do something
176 | }) {
177 | Text("Retry")
178 | }
179 | }
180 | }, loadingStateView: {
181 | Text("Loading ...")
182 | })
183 | ```
184 |
185 | For more examples take a look at the `Example` directory.
186 |
187 | ## Migration
188 |
189 |
190 | Migration 2.x -> 3.0
191 |
192 | The `AdvancedList` was dramatically simplified and is now more like the `List` and `ForEach` SwiftUI views.
193 |
194 | 1. Delete your list service instances and directly **pass your data to the list initializer**
195 | 2. Create your views through a content block (**initializer parameter**) instead of conforming your items to `View` directly (removed type erased wrapper `AnyListItem`)
196 | 3. Pass a list state binding to the initializer (**before:** the `ListService` managed the list state)
197 | 4. **Move and delete:** Instead of setting `AdvancedListActions` on your list service just pass a `onMoveAction` and/or `onDeleteAction` block to the initializer
198 |
199 | **Before:**
200 |
201 | ```swift
202 | import AdvancedList
203 |
204 | let listService = ListService()
205 | listService.supportedListActions = .moveAndDelete(onMove: { (indexSet, index) in
206 | // please move me
207 | }, onDelete: { indexSet in
208 | // please delete me
209 | })
210 | listService.listState = .loading
211 |
212 | AdvancedList(listService: listService, emptyStateView: {
213 | Text("No data")
214 | }, errorStateView: { error in
215 | VStack {
216 | Text(error.localizedDescription)
217 | .lineLimit(nil)
218 |
219 | Button(action: {
220 | // do something
221 | }) {
222 | Text("Retry")
223 | }
224 | }
225 | }, loadingStateView: {
226 | Text("Loading ...")
227 | }, pagination: .noPagination)
228 |
229 | listService.listState = .loading
230 | // fetch your items ...
231 | listService.appendItems(yourItems)
232 | listService.listState = .items
233 | ```
234 |
235 | **After:**
236 |
237 | ```swift
238 | import AdvancedList
239 |
240 | @State private var listState: ListState = .items
241 |
242 | AdvancedList(yourData, content: { item in
243 | Text("Item")
244 | }, listState: $listState, onMoveAction: { (indexSet, index) in
245 | // move me
246 | }, onDeleteAction: { indexSet in
247 | // delete me
248 | }, emptyStateView: {
249 | Text("No data")
250 | }, errorStateView: { error in
251 | VStack {
252 | Text(error.localizedDescription)
253 | .lineLimit(nil)
254 |
255 | Button(action: {
256 | // do something
257 | }) {
258 | Text("Retry")
259 | }
260 | }
261 | }, loadingStateView: {
262 | Text("Loading ...")
263 | }, pagination: .noPagination)
264 | ```
265 |
266 |
267 |
268 | Migration 3.0 -> 4.0
269 |
270 | Thanks to a hint from @SpectralDragon I could refactor the `onMove` and `onDelete` functionality to view modifiers.
271 |
272 | **Before:**
273 |
274 | ```swift
275 | import AdvancedList
276 |
277 | @State private var listState: ListState = .items
278 |
279 | AdvancedList(yourData, content: { item in
280 | Text("Item")
281 | }, listState: $listState, onMoveAction: { (indexSet, index) in
282 | // move me
283 | }, onDeleteAction: { indexSet in
284 | // delete me
285 | }, emptyStateView: {
286 | Text("No data")
287 | }, errorStateView: { error in
288 | VStack {
289 | Text(error.localizedDescription)
290 | .lineLimit(nil)
291 |
292 | Button(action: {
293 | // do something
294 | }) {
295 | Text("Retry")
296 | }
297 | }
298 | }, loadingStateView: {
299 | Text("Loading ...")
300 | }, pagination: .noPagination)
301 | ```
302 |
303 | **After:**
304 |
305 | ```swift
306 | import AdvancedList
307 |
308 | @State private var listState: ListState = .items
309 |
310 | AdvancedList(yourData, content: { item in
311 | Text("Item")
312 | }, listState: $listState, emptyStateView: {
313 | Text("No data")
314 | }, errorStateView: { error in
315 | VStack {
316 | Text(error.localizedDescription)
317 | .lineLimit(nil)
318 |
319 | Button(action: {
320 | // do something
321 | }) {
322 | Text("Retry")
323 | }
324 | }
325 | }, loadingStateView: {
326 | Text("Loading ...")
327 | }, pagination: .noPagination)
328 | .onMove { (indexSet, index) in
329 | // move me
330 | }
331 | .onDelete { indexSet in
332 | // delete me
333 | }
334 | ```
335 |
336 |
337 |
338 | Migration 4.0 -> 5.0
339 |
340 | `Pagination` is now implemented as a `modifier` 💪 And last but not least the code documentation arrived 😀
341 |
342 | **Before:**
343 |
344 | ```swift
345 | private lazy var pagination: AdvancedListPagination = {
346 | .thresholdItemPagination(errorView: { error in
347 | AnyView(
348 | VStack {
349 | Text(error.localizedDescription)
350 | .lineLimit(nil)
351 | .multilineTextAlignment(.center)
352 |
353 | Button(action: {
354 | // load current page again
355 | }) {
356 | Text("Retry")
357 | }.padding()
358 | }
359 | )
360 | }, loadingView: {
361 | AnyView(
362 | VStack {
363 | Divider()
364 | Text("Loading...")
365 | }
366 | )
367 | }, offset: 25, shouldLoadNextPage: {
368 | // load next page
369 | }, state: .idle)
370 | }()
371 |
372 | @State private var listState: ListState = .items
373 |
374 | AdvancedList(yourData, content: { item in
375 | Text("Item")
376 | }, listState: $listState, emptyStateView: {
377 | Text("No data")
378 | }, errorStateView: { error in
379 | VStack {
380 | Text(error.localizedDescription)
381 | .lineLimit(nil)
382 |
383 | Button(action: {
384 | // do something
385 | }) {
386 | Text("Retry")
387 | }
388 | }
389 | }, loadingStateView: {
390 | Text("Loading ...")
391 | }, pagination: pagination)
392 |
393 | ```
394 |
395 | **After:**
396 |
397 | ```swift
398 | @State private var listState: ListState = .items
399 | @State private var paginationState: AdvancedListPaginationState = .idle
400 |
401 | AdvancedList(yourData, content: { item in
402 | Text("Item")
403 | }, listState: $listState, emptyStateView: {
404 | Text("No data")
405 | }, errorStateView: { error in
406 | VStack {
407 | Text(error.localizedDescription)
408 | .lineLimit(nil)
409 |
410 | Button(action: {
411 | // do something
412 | }) {
413 | Text("Retry")
414 | }
415 | }
416 | }, loadingStateView: {
417 | Text("Loading ...")
418 | })
419 | .pagination(.init(type: .lastItem, shouldLoadNextPage: {
420 | paginationState = .loading
421 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
422 | items.append(contentsOf: moreItems)
423 | paginationState = .idle
424 | }
425 | }) {
426 | switch paginationState {
427 | case .idle:
428 | EmptyView()
429 | case .loading:
430 | if #available(iOS 14, *) {
431 | ProgressView()
432 | } else {
433 | Text("Loading ...")
434 | }
435 | case let .error(error):
436 | Text(error.localizedDescription)
437 | }
438 | })
439 | ```
440 |
441 |
442 |
443 | Migration 6.0 -> 7.0
444 |
445 | I replaced the unnecessary listState `Binding` and replaced it with a simple value parameter.
446 |
447 | **Before:**
448 |
449 | ```swift
450 | import AdvancedList
451 |
452 | @State private var listState: ListState = .items
453 |
454 | AdvancedList(yourData, content: { item in
455 | Text("Item")
456 | }, listState: $listState, emptyStateView: {
457 | Text("No data")
458 | }, errorStateView: { error in
459 | VStack {
460 | Text(error.localizedDescription)
461 | .lineLimit(nil)
462 |
463 | Button(action: {
464 | // do something
465 | }) {
466 | Text("Retry")
467 | }
468 | }
469 | }, loadingStateView: {
470 | Text("Loading ...")
471 | }, pagination: .noPagination)
472 | ```
473 |
474 | **After:**
475 |
476 | ```swift
477 | import AdvancedList
478 |
479 | @State private var listState: ListState = .items
480 |
481 | AdvancedList(yourData, content: { item in
482 | Text("Item")
483 | }, listState: listState, emptyStateView: {
484 | Text("No data")
485 | }, errorStateView: { error in
486 | VStack {
487 | Text(error.localizedDescription)
488 | .lineLimit(nil)
489 |
490 | Button(action: {
491 | // do something
492 | }) {
493 | Text("Retry")
494 | }
495 | }
496 | }, loadingStateView: {
497 | Text("Loading ...")
498 | }, pagination: .noPagination)
499 | ```
500 |
501 |
502 |
503 | Migration 7.0 -> 8.0
504 | Nothing to do 🎉
505 |
506 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/private/Models/AdvancedListType.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | enum AdvancedListType {
4 | case data(data: AnyRandomAccessCollection, listView: (AdvancedList.Rows) -> AnyView, rowContent: (Element) -> AnyView)
5 | case container(content: () -> AnyView)
6 | }
7 |
8 | struct AnyAdvancedListType {
9 | let value: AdvancedListType
10 |
11 | init(type: AdvancedListType) {
12 | switch type {
13 | case let .data(data, listView, rowContent):
14 | self.value = .data(
15 | data: AnyRandomAccessCollection(data.map(AnyIdentifiable.init)),
16 | listView: listView,
17 | rowContent: { rowContent($0.value as! Element) }
18 | )
19 | case let .container(content):
20 | self.value = .container(content: content)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/private/Models/AnyAdvancedListPagination.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyAdvancedListPagination.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 31.07.20.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct AnyAdvancedListPagination {
11 | let type: AdvancedListPaginationType
12 | let shouldLoadNextPage: () -> Void
13 | let content: () -> AnyView
14 |
15 | init(_ pagination: AdvancedListPagination) {
16 | type = pagination.type
17 | shouldLoadNextPage = pagination.shouldLoadNextPage
18 | content = { AnyView(pagination.content()) }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/private/Models/ListState+error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListState+error.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 02.08.20.
6 | //
7 |
8 | extension ListState {
9 | var error: Swift.Error? {
10 | guard case let ListState.error(error) = self else {
11 | return nil
12 | }
13 | return error
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/AdvancedListPagination.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListPagination.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 15.08.19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Represents the pagination configuration.
11 | public struct AdvancedListPagination {
12 | let type: AdvancedListPaginationType
13 | let shouldLoadNextPage: () -> Void
14 | let content: () -> Content
15 |
16 | /// Initializes the pagination configuration with the given values.
17 | ///
18 | /// - Parameters:
19 | /// - type: The type of the pagination, choose between `lastItem` or `thresholdItem`.
20 | /// - shouldLoadNextPage: A closure that is called everytime the end of a page is reached.
21 | /// - content: A closure providing a `View` which should be displayed at the end of a page.
22 | public init(
23 | type: AdvancedListPaginationType,
24 | shouldLoadNextPage: @escaping () -> Void,
25 | @ViewBuilder content: @escaping () -> Content
26 | ) {
27 | self.type = type
28 | self.shouldLoadNextPage = shouldLoadNextPage
29 | self.content = content
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/AdvancedListPaginationState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListPaginationState.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 17.08.19.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Represents the different states of a pagination.
11 | public enum AdvancedListPaginationState: Equatable {
12 | /// The error state; use this state if an error occurs while loading a page.
13 | case error(_ error: NSError)
14 | /// The idle state; use this state if no page loading is in progress.
15 | case idle
16 | /// The loading state; use this state if a page is loaded.
17 | case loading
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/AdvancedListPaginationType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListPaginationType.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 16.08.19.
6 | //
7 |
8 | /// Specifies the different pagination types.
9 | public enum AdvancedListPaginationType {
10 | /// Notifies the pagination configuration object when the last item in the list was reached.
11 | case lastItem
12 | /// Notifies the pagination configuration object when the given offset was passed.
13 | case thresholdItem(offset: Int)
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/AnyDynamicViewContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyDynamicViewContent.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 20.11.19.
6 | //
7 |
8 | import SwiftUI
9 |
10 | /// Type erased dynamic view content that generates views from an underlying collection of data.
11 | public struct AnyDynamicViewContent: DynamicViewContent {
12 | private let view: AnyView
13 |
14 | public let data: AnyCollection
15 |
16 | public var body: some View { view }
17 |
18 | init(_ view: View) {
19 | self.view = AnyView(view)
20 | self.data = AnyCollection(view.data.map { $0 as Any })
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/AnyIdentifiable.swift:
--------------------------------------------------------------------------------
1 | /// A type-erased `Identifiable` value.
2 | public struct AnyIdentifiable: Identifiable {
3 | public let id: AnyHashable
4 | /// The value conforming to the `Identifiable` protocol.
5 | public let value: Any
6 |
7 | /// Initializes the type-erased `Identifiable` value.
8 | ///
9 | /// - Parameter identifiable: A value conforming to the `Identifiable` protocol.
10 | public init(_ identifiable: T) {
11 | id = AnyHashable(identifiable.id)
12 | value = identifiable
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Models/ListState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListState.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 01.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Specifies the different states of an `AdvancedList`.
12 | public enum ListState: Equatable {
13 | /// The `error` state; displays the error view instead of the list to the user.
14 | case error(_ error: NSError)
15 | /// The `items` state (`default`); displays the items or the empty state view (if there are no items) to the user.
16 | case items
17 | /// The `loading` state; displays the loading state view instead of the list to the user.
18 | case loading
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/AdvancedList/public/Views/AdvancedList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedList.swift
3 | // AdvancedList
4 | //
5 | // Created by Christian Elies on 01.07.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import ListPagination
10 | import SwiftUI
11 |
12 | /// An `advanced` container that presents rows of data arranged in a single column.
13 | /// Built-in `empty`, `error` and `loading` state.
14 | /// Supports `lastItem` or `thresholdItem` pagination.
15 | public struct AdvancedList: View {
16 | // MARK: - Public
17 |
18 | public typealias OnMoveAction = Optional<(IndexSet, Int) -> Void>
19 | public typealias OnDeleteAction = Optional<(IndexSet) -> Void>
20 |
21 | // MARK: - Private
22 |
23 | private typealias Configuration = (AnyDynamicViewContent) -> AnyDynamicViewContent
24 |
25 | private let type: AnyAdvancedListType
26 |
27 | private let listState: ListState
28 | private let emptyStateView: () -> EmptyStateView
29 | private let errorStateView: (Error) -> ErrorStateView
30 | private let loadingStateView: () -> LoadingStateView
31 |
32 | private var pagination: AnyAdvancedListPagination?
33 | @State private var isLastItem: Bool = false
34 |
35 | private var configurations: [Configuration]
36 | }
37 |
38 | // MARK: - Data initializers
39 |
40 | extension AdvancedList {
41 | public typealias Rows = () -> AnyDynamicViewContent
42 |
43 | /// Initializes the list with the given values.
44 | ///
45 | /// - Parameters:
46 | /// - data: The data for populating the list.
47 | /// - listView: A view builder that creates a custom list view from the given type erased dynamic view content representing the rows of the list.
48 | /// - content: A view builder that creates the view for a single row of the list.
49 | /// - listState: A value representing the state of the list, defaults to `items`.
50 | /// - emptyStateView: A view builder that creates the view for the empty state of the list.
51 | /// - errorStateView: A view builder that creates the view for the error state of the list.
52 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
53 | public init(_ data: Data, @ViewBuilder listView: @escaping (Rows) -> ListView, @ViewBuilder content: @escaping (Data.Element) -> RowContent, listState: ListState = .items, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) where Data.Element: Identifiable {
54 | let listView = { AnyView(listView($0)) }
55 | self.type = .init(type: .data(data: AnyRandomAccessCollection(data), listView: listView, rowContent: { AnyView(content($0)) }))
56 |
57 | self.listState = listState
58 | self.emptyStateView = emptyStateView
59 | self.errorStateView = errorStateView
60 | self.loadingStateView = loadingStateView
61 | configurations = []
62 | }
63 |
64 | /// Initializes the list with the given values.
65 | /// Uses the native `SwiftUI` `List` as list view.
66 | ///
67 | /// - Parameters:
68 | /// - data: The data for populating the list.
69 | /// - content: A view builder that creates the view for a single row of the list.
70 | /// - listState: A value representing the state of the list, defaults to `items`.
71 | /// - emptyStateView: A view builder that creates the view for the empty state of the list.
72 | /// - errorStateView: A view builder that creates the view for the error state of the list.
73 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
74 | public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> RowContent, listState: ListState = .items, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) where Data.Element: Identifiable {
75 | let listView = { AnyView(List(content: $0)) }
76 | self.type = .init(type: .data(data: AnyRandomAccessCollection(data), listView: listView, rowContent: { AnyView(content($0)) }))
77 |
78 | self.listState = listState
79 | self.emptyStateView = emptyStateView
80 | self.errorStateView = errorStateView
81 | self.loadingStateView = loadingStateView
82 | configurations = []
83 | }
84 |
85 | /// Initializes the list with the given values that supports selecting a single row.
86 | /// Uses the native `SwiftUI` `List` as list view.
87 | ///
88 | /// - Parameters:
89 | /// - data: The data for populating the list.
90 | /// - rowContent: A view builder that creates the view for a single row of the list.
91 | /// - listState: A value representing the state of the list, defaults to `items`.
92 | /// - selection: A binding to a selected value.
93 | /// - emptyStateView: A view builder that creates the view for the empty state of the list.
94 | /// - errorStateView: A view builder that creates the view for the error state of the list.
95 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
96 | public init(_ data: Data, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent, listState: ListState = .items, selection: Binding?, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) where Data.Element: Identifiable {
97 | let listView = { AnyView(List(selection: selection, content: $0)) }
98 | self.type = .init(type: .data(data: AnyRandomAccessCollection(data), listView: listView, rowContent: { AnyView(rowContent($0)) }))
99 |
100 | self.listState = listState
101 | self.emptyStateView = emptyStateView
102 | self.errorStateView = errorStateView
103 | self.loadingStateView = loadingStateView
104 | configurations = []
105 | }
106 |
107 | /// Initializes the list with the given values that supports selecting multiple rows.
108 | /// Uses the native `SwiftUI` `List` as list view.
109 | ///
110 | /// - Parameters:
111 | /// - data: The data for populating the list.
112 | /// - rowContent: A view builder that creates the view for a single row of the list.
113 | /// - listState: A value representing the state of the list, defaults to `items`.
114 | /// - selection: A binding to a set that identifies selected rows.
115 | /// - emptyStateView: A view builder that creates the view for the empty state of the list.
116 | /// - errorStateView: A view builder that creates the view for the error state of the list.
117 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
118 | public init(_ data: Data, @ViewBuilder rowContent: @escaping (Data.Element) -> RowContent, listState: ListState = .items, selection: Binding>?, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) where Data.Element: Identifiable {
119 | let listView = { AnyView(List(selection: selection, content: $0)) }
120 | self.type = .init(type: .data(data: AnyRandomAccessCollection(data), listView: listView, rowContent: { AnyView(rowContent($0)) }))
121 |
122 | self.listState = listState
123 | self.emptyStateView = emptyStateView
124 | self.errorStateView = errorStateView
125 | self.loadingStateView = loadingStateView
126 | configurations = []
127 | }
128 | }
129 |
130 | // MARK: - Content initializers
131 |
132 | @available(iOS 15, *)
133 | @available(macOS 12, *)
134 | @available(tvOS 15, *)
135 | extension AdvancedList where EmptyStateView == EmptyView {
136 | /// Initializes the list with the given content.
137 | ///
138 | /// - Parameters:
139 | /// - listState: A value representing the state of the list, defaults to `items`.
140 | /// - content: A view builder that creates the content of the list.
141 | /// - errorStateView: A view builder that creates the view for the error state of the list.
142 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
143 | public init(listState: ListState = .items, @ViewBuilder content: @escaping () -> Content, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
144 | self.type = .init(type: AdvancedListType.container(content: { AnyView(content()) }))
145 | self.listState = listState
146 | self.emptyStateView = { EmptyStateView() }
147 | self.errorStateView = errorStateView
148 | self.loadingStateView = loadingStateView
149 | configurations = []
150 | }
151 |
152 | /// Initializes the list with the given content.
153 | /// Uses the native `SwiftUI` `List` as list view.
154 | ///
155 | /// - Parameters:
156 | /// - listState: A value representing the state of the list, defaults to `items`.
157 | /// - listContent: A view builder that creates the content of the list.
158 | /// - errorStateView: A view builder that creates the view for the error state of the list.
159 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
160 | public init(listState: ListState = .items, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
161 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(content: listContent)) }))
162 | self.listState = listState
163 | self.emptyStateView = { EmptyStateView() }
164 | self.errorStateView = errorStateView
165 | self.loadingStateView = loadingStateView
166 | configurations = []
167 | }
168 |
169 | /// Initializes the list with the given content that supports selecting a single row.
170 | /// Uses the native `SwiftUI` `List` as list view.
171 | ///
172 | /// - Parameters:
173 | /// - listState: A value representing the state of the list, defaults to `items`.
174 | /// - selection: A binding to a selected value.
175 | /// - listContent: A view builder that creates the content of the list.
176 | /// - errorStateView: A view builder that creates the view for the error state of the list.
177 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
178 | public init(listState: ListState = .items, selection: Binding?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
179 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
180 | self.listState = listState
181 | self.emptyStateView = { EmptyStateView() }
182 | self.errorStateView = errorStateView
183 | self.loadingStateView = loadingStateView
184 | configurations = []
185 | }
186 |
187 | /// Initializes the list with the given content that supports selecting multiple rows.
188 | /// Uses the native `SwiftUI` `List` as list view.
189 | ///
190 | /// - Parameters:
191 | /// - listState: A value representing the state of the list, defaults to `items`.
192 | /// - selection: A binding to a set that identifies selected rows.
193 | /// - listContent: A view builder that creates the content of the list.
194 | /// - errorStateView: A view builder that creates the view for the error state of the list.
195 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
196 | public init(listState: ListState = .items, selection: Binding>?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
197 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
198 | self.listState = listState
199 | self.emptyStateView = { EmptyStateView() }
200 | self.errorStateView = errorStateView
201 | self.loadingStateView = loadingStateView
202 | configurations = []
203 | }
204 | }
205 |
206 | @available(iOS 15, *)
207 | @available(macOS 12, *)
208 | @available(tvOS 15, *)
209 | extension AdvancedList where EmptyStateView == EmptyView, ErrorStateView == EmptyView {
210 | /// Initializes the list with the given content.
211 | ///
212 | /// - Parameters:
213 | /// - listState: A value representing the state of the list, defaults to `items`.
214 | /// - content: A view builder that creates the content of the list.
215 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
216 | public init(listState: ListState = .items, @ViewBuilder content: @escaping () -> Content, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
217 | self.type = .init(type: AdvancedListType.container(content: { AnyView(content()) }))
218 | self.listState = listState
219 | self.emptyStateView = { EmptyStateView() }
220 | self.errorStateView = { _ in ErrorStateView() }
221 | self.loadingStateView = loadingStateView
222 | configurations = []
223 | }
224 |
225 | /// Initializes the list with the given content.
226 | /// Uses the native `SwiftUI` `List` as list view.
227 | ///
228 | /// - Parameters:
229 | /// - listState: A value representing the state of the list, defaults to `items`.
230 | /// - listContent: A view builder that creates the content of the list.
231 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
232 | public init(listState: ListState = .items, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
233 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(content: listContent)) }))
234 | self.listState = listState
235 | self.emptyStateView = { EmptyStateView() }
236 | self.errorStateView = { _ in ErrorStateView() }
237 | self.loadingStateView = loadingStateView
238 | configurations = []
239 | }
240 |
241 | /// Initializes the list with the given content that supports selecting a single row.
242 | /// Uses the native `SwiftUI` `List` as list view.
243 | ///
244 | /// - Parameters:
245 | /// - listState: A value representing the state of the list, defaults to `items`.
246 | /// - selection: A binding to a selected value.
247 | /// - listContent: A view builder that creates the content of the list.
248 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
249 | public init(listState: ListState = .items, selection: Binding?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
250 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
251 | self.listState = listState
252 | self.emptyStateView = { EmptyStateView() }
253 | self.errorStateView = { _ in ErrorStateView() }
254 | self.loadingStateView = loadingStateView
255 | configurations = []
256 | }
257 |
258 | /// Initializes the list with the given content that supports selecting multiple rows.
259 | /// Uses the native `SwiftUI` `List` as list view.
260 | ///
261 | /// - Parameters:
262 | /// - listState: A value representing the state of the list, defaults to `items`.
263 | /// - selection: A binding to a set that identifies selected rows.
264 | /// - listContent: A view builder that creates the content of the list.
265 | /// - loadingStateView: A view builder that creates the view for the loading state of the list.
266 | public init(listState: ListState = .items, selection: Binding>?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView) {
267 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
268 | self.listState = listState
269 | self.emptyStateView = { EmptyStateView() }
270 | self.errorStateView = { _ in ErrorStateView() }
271 | self.loadingStateView = loadingStateView
272 | configurations = []
273 | }
274 | }
275 |
276 | @available(iOS 15, *)
277 | @available(macOS 12, *)
278 | @available(tvOS 15, *)
279 | extension AdvancedList where EmptyStateView == EmptyView, LoadingStateView == EmptyView {
280 | /// Initializes the list with the given content.
281 | ///
282 | /// - Parameters:
283 | /// - listState: A value representing the state of the list, defaults to `items`.
284 | /// - content: A view builder that creates the content of the list.
285 | /// - errorStateView: A view builder that creates the view for the error state of the list.
286 | public init(listState: ListState = .items, @ViewBuilder content: @escaping () -> Content, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView) {
287 | self.type = .init(type: AdvancedListType.container(content: { AnyView(content()) }))
288 | self.listState = listState
289 | self.emptyStateView = { EmptyStateView() }
290 | self.errorStateView = errorStateView
291 | self.loadingStateView = { LoadingStateView() }
292 | configurations = []
293 | }
294 |
295 | /// Initializes the list with the given content.
296 | /// Uses the native `SwiftUI` `List` as list view.
297 | ///
298 | /// - Parameters:
299 | /// - listState: A value representing the state of the list, defaults to `items`.
300 | /// - listContent: A view builder that creates the content of the list.
301 | /// - errorStateView: A view builder that creates the view for the error state of the list.
302 | public init(listState: ListState = .items, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView) {
303 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(content: listContent)) }))
304 | self.listState = listState
305 | self.emptyStateView = { EmptyStateView() }
306 | self.errorStateView = errorStateView
307 | self.loadingStateView = { LoadingStateView() }
308 | configurations = []
309 | }
310 |
311 | /// Initializes the list with the given content that supports selecting a single row.
312 | /// Uses the native `SwiftUI` `List` as list view.
313 | ///
314 | /// - Parameters:
315 | /// - listState: A value representing the state of the list, defaults to `items`.
316 | /// - selection: A binding to a selected value.
317 | /// - listContent: A view builder that creates the content of the list.
318 | /// - errorStateView: A view builder that creates the view for the error state of the list.
319 | public init(listState: ListState = .items, selection: Binding?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView) {
320 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
321 | self.listState = listState
322 | self.emptyStateView = { EmptyStateView() }
323 | self.errorStateView = errorStateView
324 | self.loadingStateView = { LoadingStateView() }
325 | configurations = []
326 | }
327 |
328 | /// Initializes the list with the given content that supports selecting multiple rows.
329 | /// Uses the native `SwiftUI` `List` as list view.
330 | ///
331 | /// - Parameters:
332 | /// - listState: A value representing the state of the list, defaults to `items`.
333 | /// - selection: A binding to a set that identifies selected rows.
334 | /// - listContent: A view builder that creates the content of the list.
335 | /// - errorStateView: A view builder that creates the view for the error state of the list.
336 | public init(listState: ListState = .items, selection: Binding>?, @ViewBuilder listContent: @escaping () -> ListContent, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView) {
337 | self.type = .init(type: AdvancedListType.container(content: { AnyView(List(selection: selection, content: listContent)) }))
338 | self.listState = listState
339 | self.emptyStateView = { EmptyStateView() }
340 | self.errorStateView = errorStateView
341 | self.loadingStateView = { LoadingStateView() }
342 | configurations = []
343 | }
344 | }
345 |
346 | extension AdvancedList {
347 | @ViewBuilder public var body: some View {
348 | switch listState {
349 | case .items:
350 | switch type.value {
351 | case let .data(data, listView, _):
352 | if !data.isEmpty {
353 | VStack {
354 | if let rows = rows() {
355 | listView({ rows })
356 | }
357 |
358 | if let pagination = pagination, isLastItem {
359 | pagination.content()
360 | }
361 | }
362 | } else {
363 | emptyStateView()
364 | }
365 | case let .container(content):
366 | content()
367 | }
368 | case .loading:
369 | loadingStateView()
370 | case let .error(error):
371 | errorStateView(error)
372 | }
373 | }
374 | }
375 |
376 | // MARK: - View modifiers
377 | extension AdvancedList {
378 | /// Sets the move action for the dynamic view.
379 | ///
380 | /// `Attention`: Works only if you initialize an `AdvancedList` with a `RandomAccessCollection`.
381 | ///
382 | /// - Parameter action: A closure that SwiftUI invokes when elements in the dynamic view are moved. The closure takes two arguments that represent the offset relative to the dynamic view’s underlying collection of data.
383 | /// - Returns: An `AdvancedList` view that calls action when elements are moved within the original view.
384 | public func onMove(perform action: OnMoveAction) -> Self {
385 | configure { AnyDynamicViewContent($0.onMove(perform: action)) }
386 | }
387 |
388 | /// Sets the deletion action for the dynamic view.
389 | ///
390 | /// `Attention`: Works only if you initialize an `AdvancedList` with a `RandomAccessCollection`.
391 | ///
392 | /// - Parameter action: The action that you want SwiftUI to perform when elements in the view are deleted. SwiftUI passes a set of indices to the closure that’s relative to the dynamic view’s underlying collection of data.
393 | /// - Returns: An `AdvancedList` view that calls action when elements are deleted from the original view.
394 | public func onDelete(perform action: OnDeleteAction) -> Self {
395 | configure { AnyDynamicViewContent($0.onDelete(perform: action)) }
396 | }
397 |
398 | /// Adds pagination to the `AdvancedList`.
399 | ///
400 | /// `Attention`: Works only if you initialize an `AdvancedList` with a `RandomAccessCollection`.
401 | ///
402 | /// - Parameter pagination: An `AdvancedListPagination` object specifying the pagination.
403 | /// - Returns: An `AdvancedList` view that calls the `shouldLoadNextPage` closure of the specified `pagination` everytime when the end of a page was reached.
404 | public func pagination(
405 | _ pagination: AdvancedListPagination
406 | ) -> Self {
407 | var result = self
408 | result.pagination = AnyAdvancedListPagination(pagination)
409 | return result
410 | }
411 | }
412 |
413 | // MARK: - Private helper
414 | private extension AdvancedList {
415 | private func configure(_ configuration: @escaping Configuration) -> Self {
416 | var result = self
417 | result.configurations.append(configuration)
418 | return result
419 | }
420 |
421 | func rows() -> AnyDynamicViewContent? {
422 | switch type.value {
423 | case let .data(data, _, _):
424 | return configurations
425 | .reduce(
426 | AnyDynamicViewContent(
427 | ForEach(data) { item in
428 | itemView(item)
429 | }
430 | )
431 | ) { (currentView, configuration) in configuration(currentView) }
432 | case .container:
433 | return nil
434 | }
435 | }
436 |
437 | @ViewBuilder
438 | func itemView(_ item: AnyIdentifiable) -> some View {
439 | switch type.value {
440 | case let .data(data, _, rowContent):
441 | rowContent(item)
442 | .onAppear {
443 | listItemAppears(item)
444 |
445 | if data.isLastItem(item) {
446 | isLastItem = true
447 | }
448 | }
449 | case .container:
450 | EmptyView()
451 | }
452 | }
453 |
454 | func listItemAppears(_ item: AnyIdentifiable) {
455 | guard let pagination = pagination else {
456 | return
457 | }
458 |
459 | switch type.value {
460 | case let .data(data, _, _):
461 | switch pagination.type {
462 | case .lastItem:
463 | if data.isLastItem(item) {
464 | pagination.shouldLoadNextPage()
465 | }
466 |
467 | case let .thresholdItem(offset):
468 | if data.isThresholdItem(
469 | offset: offset,
470 | item: item
471 | ) {
472 | pagination.shouldLoadNextPage()
473 | }
474 | }
475 | case .container: ()
476 | }
477 | }
478 | }
479 |
480 | #if DEBUG
481 | struct AdvancedList_Previews : PreviewProvider {
482 | private struct MockItem: Identifiable {
483 | let id: String = UUID().uuidString
484 | }
485 |
486 | private static let items: [MockItem] = Array(0...5).map { _ in MockItem() }
487 | @State private static var listState: ListState = .items
488 |
489 | static var previews: some View {
490 | NavigationView {
491 | AdvancedList(items, content: { element in
492 | Text(element.id)
493 | }, listState: listState, emptyStateView: {
494 | Text("No data")
495 | }, errorStateView: { error in
496 | VStack {
497 | Text(error.localizedDescription)
498 | .lineLimit(nil)
499 |
500 | Button(action: {
501 | // do something
502 | }) {
503 | Text("Retry")
504 | }
505 | }
506 | }, loadingStateView: {
507 | Text("Loading ...")
508 | })
509 | }
510 | }
511 | }
512 | #endif
513 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/AdvancedListPaginationStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListPaginationStateTests.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 21.02.20.
6 | //
7 |
8 | @testable import AdvancedList
9 | import XCTest
10 |
11 | final class AdvancedListPaginationStateTests: XCTestCase {
12 | func testEqualObjects() {
13 | let object1: AdvancedListPaginationState = .idle
14 | let object2: AdvancedListPaginationState = .idle
15 | XCTAssertEqual(object1, object2)
16 | }
17 |
18 | func testUnequalObjects() {
19 | let object1: AdvancedListPaginationState = .loading
20 | let object2: AdvancedListPaginationState = .idle
21 | XCTAssertNotEqual(object1, object2)
22 | }
23 |
24 | func testEqualError() {
25 | let error = NSError(domain: "MockDomain", code: 0, userInfo: nil)
26 | let object1: AdvancedListPaginationState = .error(error)
27 | let object2: AdvancedListPaginationState = .error(error)
28 | XCTAssertEqual(object1, object2)
29 | }
30 |
31 | func testUnequalError() {
32 | let error1 = NSError(domain: "MockDomain", code: 0, userInfo: nil)
33 | let error2 = NSError(domain: "MockDomain2", code: 1, userInfo: nil)
34 | let object1: AdvancedListPaginationState = .error(error1)
35 | let object2: AdvancedListPaginationState = .error(error2)
36 | XCTAssertNotEqual(object1, object2)
37 | }
38 |
39 | static var allTests = [
40 | ("testEqualObjects", testEqualObjects),
41 | ("testUnequalObjects", testUnequalObjects),
42 | ("testEqualError", testEqualError),
43 | ("testUnequalError", testUnequalError)
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/AdvancedListTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedListTests.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 21.02.20.
6 | //
7 |
8 | @testable import AdvancedList
9 | import SwiftUI
10 | import XCTest
11 |
12 | final class AdvancedListTests: XCTestCase {
13 | private let emptyStateString = "Empty"
14 | private lazy var emptyStateView = Text(emptyStateString)
15 | private let errorStateString = "Error"
16 | private lazy var errorStateView = Text(errorStateString)
17 | private let loadingStateString = "Loading ..."
18 | private lazy var loadingStateView = Text(loadingStateString)
19 |
20 | func testEmptyStateView() {
21 | let emptyListState: ListState = .items
22 |
23 | let items: [String] = []
24 |
25 | let view = AdvancedList(
26 | items, content: { item in Text(item) },
27 | listState: emptyListState,
28 | emptyStateView: { self.emptyStateView },
29 | errorStateView: { _ in self.errorStateView },
30 | loadingStateView: { self.loadingStateView }
31 | )
32 |
33 | do {
34 | let inspectableView = try view.body.inspect()
35 | let text = try inspectableView.text()
36 | let textString = try text.string()
37 | XCTAssertEqual(textString, emptyStateString)
38 | } catch {
39 | XCTFail("\(error)")
40 | }
41 | }
42 |
43 | func testNotEmptyStateView() {
44 | let itemsListState: ListState = .items
45 |
46 | let mockItem1 = "MockItem1"
47 | let mockItem2 = "MockItem2"
48 | let items: [String] = [mockItem1, mockItem2]
49 |
50 | let view = AdvancedList(
51 | items, content: { item in Text(item) },
52 | listState: itemsListState,
53 | emptyStateView: { self.emptyStateView },
54 | errorStateView: { _ in self.errorStateView },
55 | loadingStateView: { self.loadingStateView }
56 | )
57 |
58 | do {
59 | let inspectableView = try view.body.inspect()
60 | let vStack = try inspectableView.vStack()
61 | let list = try vStack.anyView(0).list()
62 | let anyDynamicViewContent = try list.first?.view(AnyDynamicViewContent.self)
63 | let forEach = try anyDynamicViewContent?.anyView().forEach()
64 |
65 | let firstElement = try forEach?.first?.anyView()
66 | XCTAssertEqual(try firstElement?.text().string(), mockItem1)
67 |
68 | let secondElement = try forEach?[1].anyView()
69 | XCTAssertEqual(try secondElement?.text().string(), mockItem2)
70 | } catch {
71 | XCTFail("\(error)")
72 | }
73 | }
74 |
75 | func testLoadingStateView() {
76 | let loadingListState: ListState = .loading
77 |
78 | let items: [String] = []
79 |
80 | let view = AdvancedList(
81 | items, content: { item in Text(item) },
82 | listState: loadingListState,
83 | emptyStateView: { self.emptyStateView },
84 | errorStateView: { _ in self.errorStateView },
85 | loadingStateView: { self.loadingStateView }
86 | )
87 |
88 | do {
89 | let inspectableView = try view.body.inspect()
90 | let text = try inspectableView.text()
91 | let textString = try text.string()
92 | XCTAssertEqual(textString, loadingStateString)
93 | } catch {
94 | XCTFail("\(error)")
95 | }
96 | }
97 |
98 | func testErrorStateView() {
99 | let error = NSError(domain: "MockDomain", code: 1, userInfo: nil)
100 | let errorListState: ListState = .error(error)
101 |
102 | let items: [String] = []
103 |
104 | let view = AdvancedList(
105 | items, content: { item in Text(item) },
106 | listState: errorListState,
107 | emptyStateView: { self.emptyStateView },
108 | errorStateView: { _ in self.errorStateView },
109 | loadingStateView: { self.loadingStateView }
110 | )
111 |
112 | do {
113 | let inspectableView = try view.body.inspect()
114 | let text = try inspectableView.text()
115 | let textString = try text.string()
116 | XCTAssertEqual(textString, errorStateString)
117 | } catch {
118 | XCTFail("\(error)")
119 | }
120 | }
121 |
122 | static var allTests = [
123 | ("testEmptyStateView", testEmptyStateView),
124 | ("testNotEmptyStateView", testNotEmptyStateView),
125 | ("testLoadingStateView", testLoadingStateView),
126 | ("testErrorStateView", testErrorStateView)
127 | ]
128 | }
129 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/Extensions/AdvancedList+Inspectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AdvancedList+Inspectable.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 22.02.20.
6 | //
7 |
8 | @testable import AdvancedList
9 | import ViewInspector
10 |
11 | extension AdvancedList: Inspectable {}
12 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/Extensions/AnyDynamicViewContent+Inspectable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyDynamicViewContent+Inspectable.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 22.02.20.
6 | //
7 |
8 | @testable import AdvancedList
9 | import ViewInspector
10 |
11 | extension AnyDynamicViewContent: Inspectable {}
12 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/Extensions/String+Identifiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Identifiable.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 21.02.20.
6 | //
7 |
8 | extension String: Identifiable {
9 | public var id: String { self }
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/ListStateTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListStateTests.swift
3 | // AdvancedListTests
4 | //
5 | // Created by Christian Elies on 21.02.20.
6 | //
7 |
8 | @testable import AdvancedList
9 | import XCTest
10 |
11 | final class ListStateTests: XCTestCase {
12 | func testEqualObjects() {
13 | let object1: ListState = .items
14 | let object2: ListState = .items
15 | XCTAssertEqual(object1, object2)
16 | }
17 |
18 | func testUnequalObjects() {
19 | let object1: ListState = .items
20 | let object2: ListState = .loading
21 | XCTAssertNotEqual(object1, object2)
22 | }
23 |
24 | func testEqualError() {
25 | let error = NSError(domain: "MockDomain", code: 0, userInfo: nil)
26 | let object1: ListState = .error(error)
27 | let object2: ListState = .error(error)
28 | XCTAssertEqual(object1, object2)
29 | }
30 |
31 | func testUnequalError() {
32 | let error1 = NSError(domain: "MockDomain", code: 0, userInfo: nil)
33 | let error2 = NSError(domain: "MockDomain2", code: 1, userInfo: nil)
34 | let object1: ListState = .error(error1)
35 | let object2: ListState = .error(error2)
36 | XCTAssertNotEqual(object1, object2)
37 | }
38 |
39 | static var allTests = [
40 | ("testEqualObjects", testEqualObjects),
41 | ("testUnequalObjects", testUnequalObjects),
42 | ("testEqualError", testEqualError),
43 | ("testUnequalError", testUnequalError)
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/AdvancedListTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(AdvancedListPaginationStateTests.allTests),
7 | testCase(AdvancedListTests.allTests),
8 | testCase(ListStateTests.allTests)
9 | ]
10 | }
11 | #endif
12 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import AdvancedListTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += AdvancedListTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------