├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Example
├── LICENSE
├── ListPagination.gif
├── README.md
├── Shared
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── Extensions
│ │ └── String+Identifiable.swift
│ ├── SwiftUI_List_PaginationApp.swift
│ └── Views
│ │ ├── ListPaginationExampleView.swift
│ │ └── ListPaginationThresholdExampleView.swift
├── SwiftUI-List-Pagination.entitlements
├── SwiftUI-List-Pagination.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
└── iOS
│ └── Info.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── ListPagination
│ └── public
│ └── Extensions
│ └── RandomAccessCollection+isLastItem.swift
└── Tests
├── LinuxMain.swift
└── ListPaginationTests
├── ListPaginationTests.swift
└── XCTestManifests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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/ListPagination.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chris-swift-dev/ListPagination/9ba101dde046605eb9281476971565113cf4b961/Example/ListPagination.gif
--------------------------------------------------------------------------------
/Example/README.md:
--------------------------------------------------------------------------------
1 | # List-Pagination-SwiftUI
2 |
3 | Easily implement pagination support for your SwiftUI lists
4 |
5 | This repository contains a usage example of my Swift package [ListPagination](https://github.com/crelies/ListPagination).
6 |
7 | ## Motivation
8 |
9 | In my latest SwiftUI example project I needed pagination support for the SwiftUI list view.
10 |
11 | ## Preview
12 |
13 | 
14 |
--------------------------------------------------------------------------------
/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/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUI-List-Pagination
4 | //
5 | // Created by Christian Elies on 04.08.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | struct ContentView: View {
12 | var body: some View {
13 | ListPaginationExampleView()
14 | }
15 | }
16 |
17 | #if DEBUG
18 | struct ContentView_Previews: PreviewProvider {
19 | static var previews: some View {
20 | ContentView()
21 | }
22 | }
23 | #endif
24 |
--------------------------------------------------------------------------------
/Example/Shared/Extensions/String+Identifiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Identifiable.swift
3 | // SwiftUI-List-Pagination
4 | //
5 | // Created by Christian Elies on 04.08.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | /*
10 | If you want to display an array of strings
11 | in the List view you have to specify a key path,
12 | so each string can be uniquely identified.
13 | With this extension you don't have to do that anymore.
14 | */
15 | extension String: Identifiable {
16 | public var id: String {
17 | return self
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Example/Shared/SwiftUI_List_PaginationApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI_List_PaginationApp.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 SwiftUI
10 |
11 | @main
12 | struct SwiftUI_List_PaginationApp: App {
13 | var body: some Scene {
14 | WindowGroup {
15 | ContentView()
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Example/Shared/Views/ListPaginationExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListPaginationExampleView.swift
3 | // SwiftUI-List-Pagination
4 | //
5 | // Created by Christian Elies on 03.08.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import ListPagination
10 | import SwiftUI
11 |
12 | struct ListPaginationExampleView: View {
13 | @State private var items: [String] = Array(0...24).map { "Item \($0)" }
14 | @State private var isLoading: Bool = false
15 | @State private var page: Int = 0
16 |
17 | private let pageSize: Int = 25
18 |
19 | var body: some View {
20 | NavigationView {
21 | List(items) { item in
22 | VStack(alignment: .leading) {
23 | Text(item)
24 |
25 | if isLoading && items.isLastItem(item) {
26 | Divider()
27 | ProgressView()
28 | }
29 | }.onAppear {
30 | listItemAppears(item)
31 | }
32 | }
33 | .navigationTitle("List of items")
34 | .toolbar {
35 | ToolbarItem {
36 | Text("Page index: \(page)")
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | private extension ListPaginationExampleView {
44 | func listItemAppears(_ item: Item) {
45 | if items.isLastItem(item) {
46 | isLoading = true
47 |
48 | /*
49 | Simulated async behaviour:
50 | Creates items for the next page and
51 | appends them to the list after a short delay
52 | */
53 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
54 | page += 1
55 | let moreItems = getMoreItems(forPage: page, pageSize: pageSize)
56 | items.append(contentsOf: moreItems)
57 | isLoading = false
58 | }
59 | }
60 | }
61 | }
62 |
63 | private extension ListPaginationExampleView {
64 | /*
65 | In a real app you would probably fetch data
66 | from an external API.
67 | */
68 | func getMoreItems(
69 | forPage page: Int,
70 | pageSize: Int
71 | ) -> [String] {
72 | let maximum = ((page * pageSize) + pageSize) - 1
73 | let moreItems: [String] = Array(items.count...maximum).map { "Item \($0)" }
74 | return moreItems
75 | }
76 | }
77 |
78 | #if DEBUG
79 | struct ListPaginationExampleView_Previews: PreviewProvider {
80 | static var previews: some View {
81 | ListPaginationExampleView()
82 | }
83 | }
84 | #endif
85 |
--------------------------------------------------------------------------------
/Example/Shared/Views/ListPaginationThresholdExampleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ListPaginationThresholdExampleView.swift
3 | // SwiftUI-List-Pagination
4 | //
5 | // Created by Christian Elies on 05.08.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import ListPagination
10 | import SwiftUI
11 |
12 | struct ListPaginationThresholdExampleView: View {
13 | @State private var items: [String] = Array(0...24).map { "Item \($0)" }
14 | @State private var isLoading: Bool = false
15 | @State private var page: Int = 0
16 | private let pageSize: Int = 25
17 | private let offset: Int = 10
18 |
19 | var body: some View {
20 | NavigationView {
21 | List(items) { item in
22 | VStack(alignment: .leading) {
23 | Text(item)
24 |
25 | if isLoading && items.isLastItem(item) {
26 | Divider()
27 | ProgressView()
28 | }
29 | }.onAppear {
30 | listItemAppears(item)
31 | }
32 | }
33 | .navigationTitle("List of items")
34 | .toolbar {
35 | ToolbarItem {
36 | Text("Page index: \(page)")
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | private extension ListPaginationThresholdExampleView {
44 | func listItemAppears(_ item: Item) {
45 | if items.isThresholdItem(
46 | offset: offset,
47 | item: item
48 | ) {
49 | isLoading = true
50 |
51 | /*
52 | Simulated async behaviour:
53 | Creates items for the next page and
54 | appends them to the list after a short delay
55 | */
56 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
57 | page += 1
58 | let moreItems = getMoreItems(forPage: page, pageSize: pageSize)
59 | items.append(contentsOf: moreItems)
60 | isLoading = false
61 | }
62 | }
63 | }
64 | }
65 |
66 | private extension ListPaginationThresholdExampleView {
67 | /*
68 | In a real app you would probably fetch data
69 | from an external API.
70 | */
71 | func getMoreItems(
72 | forPage page: Int,
73 | pageSize: Int
74 | ) -> [String] {
75 | let maximum = ((page * pageSize) + pageSize) - 1
76 | let moreItems: [String] = Array(items.count...maximum).map { "Item \($0)" }
77 | return moreItems
78 | }
79 | }
80 |
81 | #if DEBUG
82 | struct ListPaginationThresholdExampleView_Previews: PreviewProvider {
83 | static var previews: some View {
84 | ListPaginationThresholdExampleView()
85 | }
86 | }
87 | #endif
88 |
--------------------------------------------------------------------------------
/Example/SwiftUI-List-Pagination.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/SwiftUI-List-Pagination.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | B944336524D8AA69000A7914 /* SwiftUI_List_PaginationApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B944335324D8AA68000A7914 /* SwiftUI_List_PaginationApp.swift */; };
11 | B944336924D8AA69000A7914 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B944335524D8AA69000A7914 /* Assets.xcassets */; };
12 | B944337124D8AAE4000A7914 /* String+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DFF74F22F7881500002DA4 /* String+Identifiable.swift */; };
13 | B944337324D8AAF9000A7914 /* ListPaginationExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DFF74C22F7871C00002DA4 /* ListPaginationExampleView.swift */; };
14 | B944337524D8AAFC000A7914 /* ListPaginationThresholdExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DFF75622F84B3300002DA4 /* ListPaginationThresholdExampleView.swift */; };
15 | B944337724D8ABB3000A7914 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DFF73C22F7870800002DA4 /* ContentView.swift */; };
16 | B944337A24D8ABBE000A7914 /* ListPagination in Frameworks */ = {isa = PBXBuildFile; productRef = B944337924D8ABBE000A7914 /* ListPagination */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXFileReference section */
20 | B916B79B24D8AD1F00D1E0AB /* SwiftUI-List-Pagination.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SwiftUI-List-Pagination.entitlements"; sourceTree = ""; };
21 | B944335324D8AA68000A7914 /* SwiftUI_List_PaginationApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUI_List_PaginationApp.swift; sourceTree = ""; };
22 | B944335524D8AA69000A7914 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
23 | B944335A24D8AA69000A7914 /* SwiftUI-List-Pagination.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftUI-List-Pagination.app"; sourceTree = BUILT_PRODUCTS_DIR; };
24 | B944335C24D8AA69000A7914 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
25 | B9A1EC5D2804B4CD00810D18 /* ListPagination */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ListPagination; path = ..; sourceTree = ""; };
26 | B9DFF73C22F7870800002DA4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
27 | B9DFF74C22F7871C00002DA4 /* ListPaginationExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListPaginationExampleView.swift; sourceTree = ""; };
28 | B9DFF74F22F7881500002DA4 /* String+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Identifiable.swift"; sourceTree = ""; };
29 | B9DFF75422F8180500002DA4 /* ListPagination.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = ListPagination.gif; sourceTree = ""; };
30 | B9DFF75522F8183600002DA4 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
31 | B9DFF75622F84B3300002DA4 /* ListPaginationThresholdExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPaginationThresholdExampleView.swift; sourceTree = ""; };
32 | /* End PBXFileReference section */
33 |
34 | /* Begin PBXFrameworksBuildPhase section */
35 | B944335724D8AA69000A7914 /* Frameworks */ = {
36 | isa = PBXFrameworksBuildPhase;
37 | buildActionMask = 2147483647;
38 | files = (
39 | B944337A24D8ABBE000A7914 /* ListPagination in Frameworks */,
40 | );
41 | runOnlyForDeploymentPostprocessing = 0;
42 | };
43 | /* End PBXFrameworksBuildPhase section */
44 |
45 | /* Begin PBXGroup section */
46 | B944335224D8AA68000A7914 /* Shared */ = {
47 | isa = PBXGroup;
48 | children = (
49 | B9DFF73C22F7870800002DA4 /* ContentView.swift */,
50 | B944335324D8AA68000A7914 /* SwiftUI_List_PaginationApp.swift */,
51 | B944335524D8AA69000A7914 /* Assets.xcassets */,
52 | B9DFF74E22F7880400002DA4 /* Extensions */,
53 | B9DFF75322F788B800002DA4 /* Views */,
54 | );
55 | path = Shared;
56 | sourceTree = "";
57 | };
58 | B944335B24D8AA69000A7914 /* iOS */ = {
59 | isa = PBXGroup;
60 | children = (
61 | B944335C24D8AA69000A7914 /* Info.plist */,
62 | );
63 | path = iOS;
64 | sourceTree = "";
65 | };
66 | B9CA805C23053EC8004460BF /* Frameworks */ = {
67 | isa = PBXGroup;
68 | children = (
69 | );
70 | name = Frameworks;
71 | sourceTree = "";
72 | };
73 | B9DFF72C22F7870800002DA4 = {
74 | isa = PBXGroup;
75 | children = (
76 | B9A1EC5D2804B4CD00810D18 /* ListPagination */,
77 | B916B79B24D8AD1F00D1E0AB /* SwiftUI-List-Pagination.entitlements */,
78 | B9DFF75422F8180500002DA4 /* ListPagination.gif */,
79 | B9DFF75522F8183600002DA4 /* README.md */,
80 | B944335224D8AA68000A7914 /* Shared */,
81 | B944335B24D8AA69000A7914 /* iOS */,
82 | B9DFF73622F7870800002DA4 /* Products */,
83 | B9CA805C23053EC8004460BF /* Frameworks */,
84 | );
85 | sourceTree = "";
86 | };
87 | B9DFF73622F7870800002DA4 /* Products */ = {
88 | isa = PBXGroup;
89 | children = (
90 | B944335A24D8AA69000A7914 /* SwiftUI-List-Pagination.app */,
91 | );
92 | name = Products;
93 | sourceTree = "";
94 | };
95 | B9DFF74E22F7880400002DA4 /* Extensions */ = {
96 | isa = PBXGroup;
97 | children = (
98 | B9DFF74F22F7881500002DA4 /* String+Identifiable.swift */,
99 | );
100 | path = Extensions;
101 | sourceTree = "";
102 | };
103 | B9DFF75322F788B800002DA4 /* Views */ = {
104 | isa = PBXGroup;
105 | children = (
106 | B9DFF74C22F7871C00002DA4 /* ListPaginationExampleView.swift */,
107 | B9DFF75622F84B3300002DA4 /* ListPaginationThresholdExampleView.swift */,
108 | );
109 | path = Views;
110 | sourceTree = "";
111 | };
112 | /* End PBXGroup section */
113 |
114 | /* Begin PBXNativeTarget section */
115 | B944335924D8AA69000A7914 /* SwiftUI-List-Pagination (iOS) */ = {
116 | isa = PBXNativeTarget;
117 | buildConfigurationList = B944336B24D8AA69000A7914 /* Build configuration list for PBXNativeTarget "SwiftUI-List-Pagination (iOS)" */;
118 | buildPhases = (
119 | B944335624D8AA69000A7914 /* Sources */,
120 | B944335724D8AA69000A7914 /* Frameworks */,
121 | B944335824D8AA69000A7914 /* Resources */,
122 | );
123 | buildRules = (
124 | );
125 | dependencies = (
126 | );
127 | name = "SwiftUI-List-Pagination (iOS)";
128 | packageProductDependencies = (
129 | B944337924D8ABBE000A7914 /* ListPagination */,
130 | );
131 | productName = "SwiftUI-List-Pagination (iOS)";
132 | productReference = B944335A24D8AA69000A7914 /* SwiftUI-List-Pagination.app */;
133 | productType = "com.apple.product-type.application";
134 | };
135 | /* End PBXNativeTarget section */
136 |
137 | /* Begin PBXProject section */
138 | B9DFF72D22F7870800002DA4 /* Project object */ = {
139 | isa = PBXProject;
140 | attributes = {
141 | LastSwiftUpdateCheck = 1200;
142 | LastUpgradeCheck = 1100;
143 | ORGANIZATIONNAME = "Christian Elies";
144 | TargetAttributes = {
145 | B944335924D8AA69000A7914 = {
146 | CreatedOnToolsVersion = 12.0;
147 | };
148 | };
149 | };
150 | buildConfigurationList = B9DFF73022F7870800002DA4 /* Build configuration list for PBXProject "SwiftUI-List-Pagination" */;
151 | compatibilityVersion = "Xcode 9.3";
152 | developmentRegion = en;
153 | hasScannedForEncodings = 0;
154 | knownRegions = (
155 | en,
156 | Base,
157 | );
158 | mainGroup = B9DFF72C22F7870800002DA4;
159 | packageReferences = (
160 | B9CA805F230543F3004460BF /* XCRemoteSwiftPackageReference "ListPagination" */,
161 | );
162 | productRefGroup = B9DFF73622F7870800002DA4 /* Products */;
163 | projectDirPath = "";
164 | projectRoot = "";
165 | targets = (
166 | B944335924D8AA69000A7914 /* SwiftUI-List-Pagination (iOS) */,
167 | );
168 | };
169 | /* End PBXProject section */
170 |
171 | /* Begin PBXResourcesBuildPhase section */
172 | B944335824D8AA69000A7914 /* Resources */ = {
173 | isa = PBXResourcesBuildPhase;
174 | buildActionMask = 2147483647;
175 | files = (
176 | B944336924D8AA69000A7914 /* Assets.xcassets in Resources */,
177 | );
178 | runOnlyForDeploymentPostprocessing = 0;
179 | };
180 | /* End PBXResourcesBuildPhase section */
181 |
182 | /* Begin PBXSourcesBuildPhase section */
183 | B944335624D8AA69000A7914 /* Sources */ = {
184 | isa = PBXSourcesBuildPhase;
185 | buildActionMask = 2147483647;
186 | files = (
187 | B944337124D8AAE4000A7914 /* String+Identifiable.swift in Sources */,
188 | B944337724D8ABB3000A7914 /* ContentView.swift in Sources */,
189 | B944337524D8AAFC000A7914 /* ListPaginationThresholdExampleView.swift in Sources */,
190 | B944337324D8AAF9000A7914 /* ListPaginationExampleView.swift in Sources */,
191 | B944336524D8AA69000A7914 /* SwiftUI_List_PaginationApp.swift in Sources */,
192 | );
193 | runOnlyForDeploymentPostprocessing = 0;
194 | };
195 | /* End PBXSourcesBuildPhase section */
196 |
197 | /* Begin XCBuildConfiguration section */
198 | B944336C24D8AA69000A7914 /* Debug */ = {
199 | isa = XCBuildConfiguration;
200 | buildSettings = {
201 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
202 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
203 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
204 | CODE_SIGN_ENTITLEMENTS = "SwiftUI-List-Pagination.entitlements";
205 | CODE_SIGN_IDENTITY = "iPhone Developer";
206 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
207 | CODE_SIGN_STYLE = Manual;
208 | DEVELOPMENT_TEAM = 766K8ALVVD;
209 | ENABLE_PREVIEWS = YES;
210 | INFOPLIST_FILE = iOS/Info.plist;
211 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
212 | LD_RUNPATH_SEARCH_PATHS = (
213 | "$(inherited)",
214 | "@executable_path/Frameworks",
215 | );
216 | PRODUCT_BUNDLE_IDENTIFIER = "de.crelies.SwiftUI-List-Pagination";
217 | "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "";
218 | PRODUCT_NAME = "SwiftUI-List-Pagination";
219 | PROVISIONING_PROFILE_SPECIFIER = "match Development de.crelies.*";
220 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
221 | SDKROOT = iphoneos;
222 | SUPPORTS_MACCATALYST = YES;
223 | SWIFT_VERSION = 5.0;
224 | TARGETED_DEVICE_FAMILY = "1,2,6";
225 | };
226 | name = Debug;
227 | };
228 | B944336D24D8AA69000A7914 /* Release */ = {
229 | isa = XCBuildConfiguration;
230 | buildSettings = {
231 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
232 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
233 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
234 | CODE_SIGN_ENTITLEMENTS = "SwiftUI-List-Pagination.entitlements";
235 | CODE_SIGN_IDENTITY = "iPhone Developer";
236 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
237 | CODE_SIGN_STYLE = Manual;
238 | DEVELOPMENT_TEAM = 766K8ALVVD;
239 | ENABLE_PREVIEWS = YES;
240 | INFOPLIST_FILE = iOS/Info.plist;
241 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
242 | LD_RUNPATH_SEARCH_PATHS = (
243 | "$(inherited)",
244 | "@executable_path/Frameworks",
245 | );
246 | PRODUCT_BUNDLE_IDENTIFIER = "de.crelies.SwiftUI-List-Pagination";
247 | "PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "";
248 | PRODUCT_NAME = "SwiftUI-List-Pagination";
249 | PROVISIONING_PROFILE_SPECIFIER = "match Development de.crelies.*";
250 | "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
251 | SDKROOT = iphoneos;
252 | SUPPORTS_MACCATALYST = YES;
253 | SWIFT_VERSION = 5.0;
254 | TARGETED_DEVICE_FAMILY = "1,2,6";
255 | VALIDATE_PRODUCT = YES;
256 | };
257 | name = Release;
258 | };
259 | B9DFF74722F7870C00002DA4 /* Debug */ = {
260 | isa = XCBuildConfiguration;
261 | buildSettings = {
262 | ALWAYS_SEARCH_USER_PATHS = NO;
263 | CLANG_ANALYZER_NONNULL = YES;
264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
266 | CLANG_CXX_LIBRARY = "libc++";
267 | CLANG_ENABLE_MODULES = YES;
268 | CLANG_ENABLE_OBJC_ARC = YES;
269 | CLANG_ENABLE_OBJC_WEAK = YES;
270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
271 | CLANG_WARN_BOOL_CONVERSION = YES;
272 | CLANG_WARN_COMMA = YES;
273 | CLANG_WARN_CONSTANT_CONVERSION = YES;
274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
277 | CLANG_WARN_EMPTY_BODY = YES;
278 | CLANG_WARN_ENUM_CONVERSION = YES;
279 | CLANG_WARN_INFINITE_RECURSION = YES;
280 | CLANG_WARN_INT_CONVERSION = YES;
281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
285 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
286 | CLANG_WARN_STRICT_PROTOTYPES = YES;
287 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
288 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
289 | CLANG_WARN_UNREACHABLE_CODE = YES;
290 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
291 | COPY_PHASE_STRIP = NO;
292 | DEBUG_INFORMATION_FORMAT = dwarf;
293 | ENABLE_STRICT_OBJC_MSGSEND = YES;
294 | ENABLE_TESTABILITY = YES;
295 | GCC_C_LANGUAGE_STANDARD = gnu11;
296 | GCC_DYNAMIC_NO_PIC = NO;
297 | GCC_NO_COMMON_BLOCKS = YES;
298 | GCC_OPTIMIZATION_LEVEL = 0;
299 | GCC_PREPROCESSOR_DEFINITIONS = (
300 | "DEBUG=1",
301 | "$(inherited)",
302 | );
303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
305 | GCC_WARN_UNDECLARED_SELECTOR = YES;
306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
307 | GCC_WARN_UNUSED_FUNCTION = YES;
308 | GCC_WARN_UNUSED_VARIABLE = YES;
309 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
310 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
311 | MTL_FAST_MATH = YES;
312 | ONLY_ACTIVE_ARCH = YES;
313 | SDKROOT = iphoneos;
314 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
315 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
316 | };
317 | name = Debug;
318 | };
319 | B9DFF74822F7870C00002DA4 /* Release */ = {
320 | isa = XCBuildConfiguration;
321 | buildSettings = {
322 | ALWAYS_SEARCH_USER_PATHS = NO;
323 | CLANG_ANALYZER_NONNULL = YES;
324 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
326 | CLANG_CXX_LIBRARY = "libc++";
327 | CLANG_ENABLE_MODULES = YES;
328 | CLANG_ENABLE_OBJC_ARC = YES;
329 | CLANG_ENABLE_OBJC_WEAK = YES;
330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
331 | CLANG_WARN_BOOL_CONVERSION = YES;
332 | CLANG_WARN_COMMA = YES;
333 | CLANG_WARN_CONSTANT_CONVERSION = YES;
334 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
336 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
337 | CLANG_WARN_EMPTY_BODY = YES;
338 | CLANG_WARN_ENUM_CONVERSION = YES;
339 | CLANG_WARN_INFINITE_RECURSION = YES;
340 | CLANG_WARN_INT_CONVERSION = YES;
341 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
342 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
343 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
344 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
345 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
346 | CLANG_WARN_STRICT_PROTOTYPES = YES;
347 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
348 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
349 | CLANG_WARN_UNREACHABLE_CODE = YES;
350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
351 | COPY_PHASE_STRIP = NO;
352 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
353 | ENABLE_NS_ASSERTIONS = NO;
354 | ENABLE_STRICT_OBJC_MSGSEND = YES;
355 | GCC_C_LANGUAGE_STANDARD = gnu11;
356 | GCC_NO_COMMON_BLOCKS = YES;
357 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
358 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
359 | GCC_WARN_UNDECLARED_SELECTOR = YES;
360 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
361 | GCC_WARN_UNUSED_FUNCTION = YES;
362 | GCC_WARN_UNUSED_VARIABLE = YES;
363 | IPHONEOS_DEPLOYMENT_TARGET = 13.0;
364 | MTL_ENABLE_DEBUG_INFO = NO;
365 | MTL_FAST_MATH = YES;
366 | SDKROOT = iphoneos;
367 | SWIFT_COMPILATION_MODE = wholemodule;
368 | SWIFT_OPTIMIZATION_LEVEL = "-O";
369 | VALIDATE_PRODUCT = YES;
370 | };
371 | name = Release;
372 | };
373 | /* End XCBuildConfiguration section */
374 |
375 | /* Begin XCConfigurationList section */
376 | B944336B24D8AA69000A7914 /* Build configuration list for PBXNativeTarget "SwiftUI-List-Pagination (iOS)" */ = {
377 | isa = XCConfigurationList;
378 | buildConfigurations = (
379 | B944336C24D8AA69000A7914 /* Debug */,
380 | B944336D24D8AA69000A7914 /* Release */,
381 | );
382 | defaultConfigurationIsVisible = 0;
383 | defaultConfigurationName = Release;
384 | };
385 | B9DFF73022F7870800002DA4 /* Build configuration list for PBXProject "SwiftUI-List-Pagination" */ = {
386 | isa = XCConfigurationList;
387 | buildConfigurations = (
388 | B9DFF74722F7870C00002DA4 /* Debug */,
389 | B9DFF74822F7870C00002DA4 /* Release */,
390 | );
391 | defaultConfigurationIsVisible = 0;
392 | defaultConfigurationName = Release;
393 | };
394 | /* End XCConfigurationList section */
395 |
396 | /* Begin XCRemoteSwiftPackageReference section */
397 | B9CA805F230543F3004460BF /* XCRemoteSwiftPackageReference "ListPagination" */ = {
398 | isa = XCRemoteSwiftPackageReference;
399 | repositoryURL = "https://github.com/crelies/ListPagination";
400 | requirement = {
401 | kind = upToNextMajorVersion;
402 | minimumVersion = 0.1.0;
403 | };
404 | };
405 | /* End XCRemoteSwiftPackageReference section */
406 |
407 | /* Begin XCSwiftPackageProductDependency section */
408 | B944337924D8ABBE000A7914 /* ListPagination */ = {
409 | isa = XCSwiftPackageProductDependency;
410 | package = B9CA805F230543F3004460BF /* XCRemoteSwiftPackageReference "ListPagination" */;
411 | productName = ListPagination;
412 | };
413 | /* End XCSwiftPackageProductDependency section */
414 | };
415 | rootObject = B9DFF72D22F7870800002DA4 /* Project object */;
416 | }
417 |
--------------------------------------------------------------------------------
/Example/SwiftUI-List-Pagination.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/SwiftUI-List-Pagination.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/SwiftUI-List-Pagination.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": "1557cfe6508f5ea7361dbec8be6d27a2392a4015",
10 | "version": "0.1.0"
11 | }
12 | }
13 | ]
14 | },
15 | "version": 1
16 | }
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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: "ListPagination",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .tvOS(.v13)
12 | ],
13 | products: [
14 | .library(
15 | name: "ListPagination",
16 | targets: ["ListPagination"]),
17 | ],
18 | targets: [
19 | .target(
20 | name: "ListPagination",
21 | dependencies: []),
22 | .testTarget(
23 | name: "ListPaginationTests",
24 | dependencies: ["ListPagination"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ListPagination
2 |
3 | [](https://developer.apple.com/swift)
4 | [](https://www.apple.com)
5 | [](https://en.wikipedia.org/wiki/MIT_License)
6 |
7 | This Swift package provides extensions of **RandomAccessCollection** which help you add pagination support to your **SwiftUI** `List view`. It is already integrated in my [AdvancedList view (Wrapper around SwiftUI's List view)](https://github.com/crelies/AdvancedList).
8 |
9 | ## Installation
10 |
11 | Add this Swift package in Xcode using its Github repository url. (File > Swift Packages > Add Package Dependency...)
12 |
13 | ## How to use
14 |
15 | You can add pagination with two different approaches to your `List`: **Last item approach** and **Threshold item approach**.
16 |
17 | That's way this package adds two functions to **RandomAccessCollection**:
18 |
19 | ### isLastItem
20 |
21 | Use this function to check if the item in the current `List item iteration` is the last item of your collection.
22 |
23 | ### isThresholdItem
24 |
25 | With this function you can find out if the item of the current `List item iteration` is the item at your defined threshold.
26 | Pass an offset (distance to the last item) to the function so the threshold item can be determined.
27 |
28 | ## Example
29 |
30 | Both example code snippets below require a simple extension of **String**:
31 |
32 | ```swift
33 | /*
34 | If you want to display an array of strings
35 | in the List view you have to specify a key path,
36 | so each string can be uniquely identified.
37 | With this extension you don't have to do that anymore.
38 | */
39 | extension String: Identifiable {
40 | public var id: String {
41 | return self
42 | }
43 | }
44 | ```
45 |
46 | ### Last item approach
47 |
48 | ```swift
49 | struct ListPaginationExampleView: View {
50 | @State private var items: [String] = Array(0...24).map { "Item \($0)" }
51 | @State private var isLoading: Bool = false
52 | @State private var page: Int = 0
53 | private let pageSize: Int = 25
54 |
55 | var body: some View {
56 | NavigationView {
57 | List(items) { item in
58 | VStack(alignment: .leading) {
59 | Text(item)
60 |
61 | if self.isLoading && self.items.isLastItem(item) {
62 | Divider()
63 | Text("Loading ...")
64 | .padding(.vertical)
65 | }
66 | }.onAppear {
67 | self.listItemAppears(item)
68 | }
69 | }
70 | .navigationBarTitle("List of items")
71 | .navigationBarItems(trailing: Text("Page index: \(page)"))
72 | }
73 | }
74 | }
75 |
76 | extension ListPaginationExampleView {
77 | private func listItemAppears(_ item: Item) {
78 | if items.isLastItem(item) {
79 | isLoading = true
80 |
81 | /*
82 | Simulated async behaviour:
83 | Creates items for the next page and
84 | appends them to the list after a short delay
85 | */
86 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
87 | self.page += 1
88 | let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
89 | self.items.append(contentsOf: moreItems)
90 |
91 | self.isLoading = false
92 | }
93 | }
94 | }
95 | }
96 | ```
97 |
98 | ### Threshold item approach
99 |
100 | ```swift
101 | struct ListPaginationThresholdExampleView: View {
102 | @State private var items: [String] = Array(0...24).map { "Item \($0)" }
103 | @State private var isLoading: Bool = false
104 | @State private var page: Int = 0
105 | private let pageSize: Int = 25
106 | private let offset: Int = 10
107 |
108 | var body: some View {
109 | NavigationView {
110 | List(items) { item in
111 | VStack(alignment: .leading) {
112 | Text(item)
113 |
114 | if self.isLoading && self.items.isLastItem(item) {
115 | Divider()
116 | Text("Loading ...")
117 | .padding(.vertical)
118 | }
119 | }.onAppear {
120 | self.listItemAppears(item)
121 | }
122 | }
123 | .navigationBarTitle("List of items")
124 | .navigationBarItems(trailing: Text("Page index: \(page)"))
125 | }
126 | }
127 | }
128 |
129 | extension ListPaginationThresholdExampleView {
130 | private func listItemAppears(_ item: Item) {
131 | if items.isThresholdItem(offset: offset,
132 | item: item) {
133 | isLoading = true
134 |
135 | /*
136 | Simulated async behaviour:
137 | Creates items for the next page and
138 | appends them to the list after a short delay
139 | */
140 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) {
141 | self.page += 1
142 | let moreItems = self.getMoreItems(forPage: self.page, pageSize: self.pageSize)
143 | self.items.append(contentsOf: moreItems)
144 |
145 | self.isLoading = false
146 | }
147 | }
148 | }
149 | }
150 | ```
151 |
--------------------------------------------------------------------------------
/Sources/ListPagination/public/Extensions/RandomAccessCollection+isLastItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RandomAccessCollection+isLastItem.swift
3 | // ListPagination
4 | //
5 | // Created by Christian Elies on 04.08.19.
6 | // Copyright © 2019 Christian Elies. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension RandomAccessCollection where Self.Element: Identifiable {
12 | public func isLastItem(_ item: Item) -> Bool {
13 | guard !isEmpty else {
14 | return false
15 | }
16 |
17 | guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
18 | return false
19 | }
20 |
21 | let distance = self.distance(from: itemIndex, to: endIndex)
22 | return distance == 1
23 | }
24 |
25 | public func isThresholdItem(
26 | offset: Int,
27 | item: Item
28 | ) -> Bool {
29 | guard !isEmpty else {
30 | return false
31 | }
32 |
33 | guard let itemIndex = lastIndex(where: { AnyHashable($0.id) == AnyHashable(item.id) }) else {
34 | return false
35 | }
36 |
37 | let distance = self.distance(from: itemIndex, to: endIndex)
38 | let offset = offset < count ? offset : count - 1
39 | return offset == (distance - 1)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import ListPaginationTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += ListPaginationTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/ListPaginationTests/ListPaginationTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import ListPagination
3 |
4 | final class ListPaginationTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual("Hello, World!", "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/ListPaginationTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(ListPaginationTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------