├── .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 | ![Animated preview image](https://github.com/crelies/AdvancedList-SwiftUI/blob/master/AdvancedList.gif) 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 | [![Swift 5.3](https://img.shields.io/badge/swift-5.3-green.svg?longCache=true&style=flat-square)](https://developer.apple.com/swift) 4 | [![Platforms](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20tvOS-lightgrey.svg?longCache=true&style=flat-square)](https://www.apple.com) 5 | [![Current version](https://img.shields.io/github/v/tag/crelies/AdvancedList?longCache=true&style=flat-square)](https://github.com/crelies/AdvancedList) 6 | [![Build status](https://github.com/crelies/AdvancedList/actions/workflows/build.yml/badge.svg)](https://github.com/crelies/AdvancedList/actions/workflows/build.yml) 7 | [![Code coverage](https://codecov.io/gh/crelies/AdvancedList/branch/dev/graph/badge.svg?token=DhJyoUKNPM)](https://codecov.io/gh/crelies/AdvancedList) 8 | [![License](https://img.shields.io/badge/license-MIT-lightgrey.svg?longCache=true&style=flat-square)](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 | --------------------------------------------------------------------------------