├── Doc
├── hero-image.gif
├── xcode-setup.jpg
└── preview-banner.jpg
├── PageViewSample
├── PageViewSample
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── PageViewSample.entitlements
│ ├── PageViewSampleApp.swift
│ └── ContentView.swift
└── PageViewSample.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── juniperphoton.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
│ ├── xcuserdata
│ └── juniperphoton.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── project.pbxproj
├── .swiftpm
└── xcode
│ └── xcuserdata
│ └── juniperphoton.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── PageView
└── PageViewSwiftUI.swift
/Doc/hero-image.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/PageView.SwiftUI/HEAD/Doc/hero-image.gif
--------------------------------------------------------------------------------
/Doc/xcode-setup.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/PageView.SwiftUI/HEAD/Doc/xcode-setup.jpg
--------------------------------------------------------------------------------
/Doc/preview-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/PageView.SwiftUI/HEAD/Doc/preview-banner.jpg
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/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 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/project.xcworkspace/xcuserdata/juniperphoton.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JuniperPhoton/PageView.SwiftUI/HEAD/PageViewSample/PageViewSample.xcodeproj/project.xcworkspace/xcuserdata/juniperphoton.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "nuke",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/kean/Nuke.git",
7 | "state" : {
8 | "revision" : "f67266f176af4add9f7a7020486826d82d562473",
9 | "version" : "12.1.2"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/PageViewSample.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 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/PageViewSampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageViewSampleApp.swift
3 | // PageViewSample
4 | //
5 | // Created by Photon Juniper on 2023/1/7.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct PageViewSampleApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | PageViewSample()
15 | #if os(macOS)
16 | .frame(minWidth: 600, minHeight: 500)
17 | #endif
18 | }
19 |
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/xcuserdata/juniperphoton.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | PageViewSample.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/juniperphoton.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | PageView.SwiftUI-Package.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | PageView.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 2
16 |
17 |
18 | SuppressBuildableAutocreation
19 |
20 | PageView.SwiftUI
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 JuniperPhoton
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.7
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: "PageViewSwiftUI",
8 | platforms: [
9 | .iOS(.v14),
10 | .macOS(.v11),
11 | .watchOS(.v8),
12 | .tvOS(.v15)
13 | ],
14 | products: [
15 | // Products define the executables and libraries a package produces, and make them visible to other packages.
16 | .library(
17 | name: "PageView",
18 | targets: ["PageView"]),
19 | ],
20 | dependencies: [
21 | // Dependencies declare other packages that this package depends on.
22 | // .package(url: /* package url */, from: "1.0.0"),
23 | ],
24 | targets: [
25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
26 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
27 | .target(
28 | name: "PageView",
29 | dependencies: [])
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // PageViewSample
4 | //
5 | // Created by Photon Juniper on 2023/1/7.
6 | //
7 |
8 | import SwiftUI
9 | import PageView
10 | import NukeUI
11 |
12 | enum Page: String, Equatable, Identifiable, Hashable, CaseIterable {
13 | case new = "New"
14 | case featured = "Featured"
15 | case random = "Random"
16 | case search = "Search"
17 |
18 | var id: String {
19 | return self.rawValue
20 | }
21 | }
22 |
23 | enum Tabs: String, Hashable, CaseIterable {
24 | case new = "New"
25 | case downloaded = "Downloaded"
26 | case profile = "Profile"
27 | }
28 |
29 | struct PageViewSample: View {
30 | @State var selectedTab = Tabs.new
31 |
32 | var body: some View {
33 | BannerView()
34 | }
35 | }
36 |
37 | class Banner: Equatable, Identifiable, Hashable {
38 | static func == (lhs: Banner, rhs: Banner) -> Bool {
39 | return lhs.imageUrl == rhs.imageUrl
40 | }
41 |
42 | let imageUrl: String
43 | let name: String
44 |
45 | init(imageUrl: String, name: String) {
46 | self.imageUrl = imageUrl
47 | self.name = name
48 | }
49 |
50 | func hash(into hasher: inout Hasher) {
51 | hasher.combine(imageUrl)
52 | }
53 | }
54 |
55 | struct BannerView: View {
56 | @State var items: [Banner] = [
57 | Banner(imageUrl: "https://images.unsplash.com/photo-1672824528354-784dea42edaa?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1675&q=80", name: "Snow land"),
58 | Banner(imageUrl: "https://images.unsplash.com/photo-1672207163711-d57124315946?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2091&q=80", name: "Landscape"),
59 | Banner(imageUrl: "https://images.unsplash.com/photo-1667569700688-dca9fa7430a8?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2054&q=80", name: "Island"),
60 | Banner(imageUrl: "https://images.unsplash.com/photo-1667298026326-bf93f52460e0?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1675&q=80", name: "Viliage"),
61 | Banner(imageUrl: "https://images.unsplash.com/photo-1662555320245-0c113fe1b87c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2832&q=80", name: "Road"),
62 | Banner(imageUrl: "https://images.unsplash.com/photo-1661256195466-abf9df7d3e2c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2178&q=80", name: "Morning"),
63 | ]
64 |
65 | @State var pageIndex: Int = 0
66 |
67 | @Namespace var namespace
68 |
69 | var body: some View {
70 | VStack(alignment: .leading) {
71 | HStack {
72 | Text("Gallery")
73 | .bold().font(.largeTitle)
74 |
75 | HStack(spacing: 0) {
76 | ZStack {
77 | Text("\(pageIndex + 1)")
78 | .tracking(4)
79 | .font(.title.bold())
80 | .foregroundColor(Color.accentColor)
81 | .id(pageIndex)
82 | .transition(.move(edge: .bottom))
83 | }.clipped()
84 |
85 | Text("/\(items.count)")
86 | .tracking(4)
87 | .font(.title.bold())
88 | .foregroundColor(Color.accentColor)
89 | }
90 |
91 | }.padding()
92 |
93 | ZStack {
94 | PageView(items: items,
95 | pageIndex: $pageIndex,
96 | disablePaging: Binding.constant(false), spacing: 8) { item in
97 | ImageItemView(item: item)
98 | }
99 |
100 | indicatorView
101 | }
102 | .frame(maxWidth: .infinity)
103 | .padding(.vertical)
104 | }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
105 | }
106 |
107 | private var indicatorView: some View {
108 | HStack {
109 | ForEach(items, id: \.id) { item in
110 | Circle().fill(Color.white.opacity(0.3))
111 | .frame(width: 12, height: 12)
112 | .background {
113 | if item == items[pageIndex] {
114 | Circle().fill(Color.white)
115 | .matchedGeometryEffect(id: "indicator", in: namespace)
116 | }
117 | }
118 | .onTapGesture {
119 | pageIndex = items.firstIndex(of: item)!
120 | }
121 | }
122 | }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
123 | .padding()
124 | }
125 | }
126 |
127 | struct ImageItemView: View {
128 | let item: Banner
129 |
130 | var body: some View {
131 | GeometryReader { reader in
132 | ZStack {
133 | LazyImage(request: ImageRequest(stringLiteral: item.imageUrl)) { state in
134 | if state.isLoading {
135 | Rectangle().fill(Color.gray.opacity(0.5))
136 | } else if let image = state.image {
137 | image.scaledToFill()
138 | .frame(width: reader.size.width, height: reader.size.height)
139 | }
140 | }
141 | .frame(width: reader.size.width, height: reader.size.height)
142 | .cornerRadius(12)
143 | .shadow(radius: 4)
144 |
145 | Text(item.name)
146 | .foregroundColor(Color.white)
147 | .font(.largeTitle.bold())
148 | .padding()
149 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
150 | .shadow(radius: 4)
151 | }
152 | }.padding(.horizontal)
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PageView.SwiftUI
2 |
3 |
4 |
5 | A container view that provides paging with virtualization/lazy loading feature, being implemented purely in `SwiftUI` using `HStack` and `DragGesture` internally.
6 |
7 | > Since it's a native view from SwiftUI, it supports iOS, iPadOS and macOS.
8 |
9 | PageView takes care of the views displaying in the screen, and will discard the views when they are offscreen, which is defined by ``offscreenCountPerSide``.
10 |
11 | Users can swipe horizontally to switch pages. You provides ``pageIndex`` binding to get or set the current page.
12 |
13 | To get more paging info such as paging progress, you pass the ``onPageTranslationChanged`` block and you will get noticed when paging translation changed.
14 |
15 | > NOTE: On iOS, iPadOS and tvOS, if you just need a container view as a navigating root view with swiping to switch pages, you can just use the official ``TabView`` with .page `TabViewStyle`. However, if you want to display a large amount of data, this ``PageView`` is for you since it has virtualization/lazy loading feature.
16 |
17 | > NOTE: `DragGesture` doesn't support Mac's TrackPad, so if you need to support swiping-to-scroll by TrackPad, you must use `NSPageController` to implement this. I make a `NSPageView` based on `NSPageController`, see https://github.com/JuniperPhoton/MyerLibApple/blob/main/Sources/MyerView/Mac/NSPageView.swift for more.
18 |
19 | ## Import using Swift Package
20 |
21 | 
22 |
23 | Add as package dependencies in your Xcode, like below.
24 |
25 | ```
26 | https://github.com/JuniperPhoton/PageView.SwiftUI
27 | ```
28 |
29 | Before using PageView, remember to import:
30 |
31 | ```swift
32 | import PageView
33 | ```
34 |
35 | ## Example
36 |
37 | The first look:
38 |
39 | ```swift
40 | PageView(items: items,
41 | pageIndex: $pageIndex,
42 | disablePaging: Binding.constant(false), spacing: 8) { item in
43 | ZStack {
44 | AsyncImage(...)
45 |
46 | Text(...)
47 | }
48 | }
49 | ```
50 |
51 | The example above achieves the following feature:
52 |
53 | 
54 |
55 | The indicator uses the binding `pageIndex` to update itself:
56 |
57 | ```swift
58 | HStack {
59 | ForEach(items, id: \.id) { item in
60 | Circle().fill(Color.white)
61 | .opacity(items.firstIndex(of: item) == pageIndex ? 1.0 : 0.5)
62 | .frame(width: 12, height: 12)
63 | }
64 | }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
65 | .padding()
66 | ```
67 |
68 | ## Customization
69 |
70 | First please refer to the initializer of the ``PageView``.
71 |
72 | - Parameter `items`: items to be populated, should be a ``RandomAccessCollection``
73 | - Parameter `pageIndex`: a binding to the current page index
74 | - Parameter `disablePaging`: a binding to disable the paging. Set this to true will disable the gesture, you can still set the ``pageIndex`` to navigate to the specified page
75 | - Parameter `offscreenCountPerSide`: effects how many Views will be on screen at the same time. The default value is 2, which makes ``1 + 2 * 2 = 5`` views. Your ``items`` could be as large as you want, and keep this ``offscreenCountPerSide`` small to help reduce your memory footage.
76 | - Parameter `spacing`: spacing between pages, horizontally. Note that the spacing won't be see until users start swiping
77 | - Parameter `scrollSlop`: how much pts the user swipe to navigate to the next page, default to 20pt
78 | - Parameter `animationDuration`: animation duration, default to 0.3 seconds
79 | - Parameter `onPageTranslationChanged`: when the user start swiping, this block will be invoked to provide information about paging translation. See ``PagingTranslation`` to know more.
80 | - Parameter `itemContent`: provides ``View`` given a ``C.Element`` you passed in the ``items``
81 |
82 | ```swift
83 | public init(items: C,
84 | pageIndex: Binding,
85 | disablePaging: Binding,
86 | offscreenCountPerSide: Int = 2,
87 | spacing: CGFloat = 20,
88 | scrollSlop: CGFloat = 20,
89 | animationDuration: CGFloat = 0.3,
90 | onPageTranslationChanged: ((PagingTranslation) -> Void)? = nil,
91 | @ViewBuilder itemContent: @escaping (C.Element) -> Content)
92 | ```
93 |
94 | The example of `onPageTranslationChanged` will output when swiping from `page0` to `page1`. You can use this progress to update your indicator progressively.
95 |
96 | ```
97 | app current translation is 0 -> 1, Progress: 0.028837985361502068
98 | app current translation is 0 -> 1, Progress: 0.03816793893129771
99 | app current translation is 0 -> 1, Progress: 0.05173874872028069
100 | app current translation is 0 -> 1, Progress: 0.05597964376590331
101 | app current translation is 0 -> 1, Progress: 0.057675970723004136
102 | app current translation is 0 -> 1, Progress: 0.05937233650654024
103 | app current translation is 0 -> 1, Progress: 0.06446139503071327
104 | app current translation is 0 -> 1, Progress: 0.07803220481969625
105 | app current translation is 0 -> 1, Progress: 0.09499574617575143
106 | app current translation is 0 -> 1, Progress: 0.11535198027244355
107 | app current translation is 0 -> 1, Progress: 0.14079727289330868
108 | app current translation is 0 -> 1, Progress: 0.15521628498727735
109 | app current translation is 0 -> 1, Progress: 0.1798133753031568
110 | app current translation is 0 -> 1, Progress: 0.20610687022900764
111 | app current translation is 0 -> 1, Progress: 0.24173027989821882
112 | app current translation is 0 -> 1, Progress: 1.0
113 | ```
114 |
115 | ## Example
116 |
117 | This repo includes an example, please navigate to [this](https://github.com/JuniperPhoton/PageView.SwiftUI/tree/main/PageViewSample) to know more.
118 |
119 | ## Limitations
120 |
121 | There are some limitations which are about to be resolved(perhaps we don't have enough API to do this):
122 | - Inside the ``PageView``, you can put a SwiftUI ``List`` and it works fine. But if you put a ``ScrollView`` with grids or stacks, ``PageView`` would take over the gesture and the ``ScrollView`` can't be scrolled. Currently SwiftUI can't handle the conflict of your custom gesture with other built-in views'.
123 |
--------------------------------------------------------------------------------
/Sources/PageView/PageViewSwiftUI.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Provide information about paging.
4 | ///
5 | /// You use the ``currentIndex`` to get the current paging index. The ``currentIndex`` won't be updated until the swiping ends.
6 | /// You use the ``nextIndex`` to get the predicated next index. The framework guarantees that the ``nextIndex`` will be safe to use and won't cause out-of-bounds issues.
7 | /// In the LTR context, If the ``currentIndex`` is 0 and the user is swiping to the right, the ``nextIndex`` should be 0 too.
8 | ///
9 | /// You use the ``progress`` to get the swiping progress between pages. Useful when you building a indicator which follows the paging progress.
10 | public struct PagingTranslation: CustomStringConvertible, Equatable {
11 | let currentIndex: Int
12 | let nextIndex: Int
13 | let progress: CGFloat
14 |
15 | public init(currentIndex: Int, nextIndex: Int, progress: CGFloat) {
16 | self.currentIndex = currentIndex
17 | self.nextIndex = nextIndex
18 | self.progress = progress
19 | }
20 |
21 | public var description: String {
22 | return "\(currentIndex) -> \(nextIndex), Progress: \(progress)"
23 | }
24 | }
25 |
26 | /// A container view providing paging with virtualization feature.
27 | ///
28 | /// PageView takes care of the views displaying in the screen, and will discard the views offscreen(which is defined by ``offscreenCountPerSide``.
29 | ///
30 | /// Users can swipe horizontally to switch pages. You provides ``pageIndex`` binding to get or set the current page.
31 | /// To get more paging info like paging progress, you pass the ``onPageTranslationChanged`` block and you will get noticed when paging translation changed.
32 | ///
33 | /// See the initializer to know more customations.
34 | public struct PageView: View where Data: Equatable & Hashable,
35 | Data.Element: Identifiable & Equatable {
36 | let items: Data
37 | let itemContent: (Data.Element) -> Content
38 | let pageIndex: Binding
39 |
40 | let disablePaging: Binding
41 |
42 | var onPageTranslationChanged: ((PagingTranslation) -> Void)? = nil
43 |
44 | @GestureState var dragTranslationX: CGFloat = 0
45 |
46 | @State var translationX: CGFloat = 0
47 | @State var virtualPageIndex: CGFloat = 0
48 |
49 | @State var displayedItemId: Data.Element.ID? = nil {
50 | didSet {
51 | if let originalIndex = items.firstIndex(where: { v in
52 | v.id == displayedItemId
53 | }) as? Int {
54 | pageIndex.wrappedValue = originalIndex
55 | }
56 | }
57 | }
58 |
59 | @State var displayedItems: [Data.Element] = []
60 | @State var width: CGFloat = 0.0
61 |
62 | private let offscreenCountPerSide: Int
63 | private let spacing: CGFloat
64 | private let scrollSlop: CGFloat
65 | private let animationDuration: CGFloat
66 |
67 | /// - Parameter items: items to be populated, should be a ``RandomAccessCollection``
68 | /// - Parameter pageIndex: a binding to the current page index
69 | /// - Parameter disablePaging: a binding to disable the paging. Set this to true will disable the gesture, you can still set the ``pageIndex`` to navigate to the specified page
70 | /// - Parameter offscreenCountPerSide: effects how many Views will be on screen at the same time. The default value is 2, which makes ``1 + 2 * 2 = 5`` views. Your ``items`` could be as large as you want, and keep this ``offscreenCountPerSide`` small to help reduce your memory footage.
71 | /// - Parameter spacing: spacing between pages, horizontally. Note that the spacing won't be see until users start swiping
72 | /// - Parameter scrollSlop: how much pts the user swipe to navigate to the next page, default to 20pt
73 | /// - Parameter animationDuration: animation duration, default to 0.3 seconds
74 | /// - Parameter onPageTranslationChanged: when the user start swiping, this block will be invoked to provide information about paging translation. See ``PagingTranslation`` to know more.
75 | /// - Parameter itemContent: provides ``View`` given a ``Data.Element`` you passed in the ``items``
76 | public init(items: Data,
77 | pageIndex: Binding,
78 | disablePaging: Binding = .constant(false),
79 | offscreenCountPerSide: Int = 2,
80 | spacing: CGFloat = 20,
81 | scrollSlop: CGFloat = 20,
82 | animationDuration: CGFloat = 0.3,
83 | onPageTranslationChanged: ((PagingTranslation) -> Void)? = nil,
84 | @ViewBuilder itemContent: @escaping (Data.Element) -> Content) {
85 | self.items = items
86 | self.itemContent = itemContent
87 | self.pageIndex = pageIndex
88 | self.disablePaging = disablePaging
89 | self.offscreenCountPerSide = offscreenCountPerSide
90 | self.spacing = spacing
91 | self.scrollSlop = scrollSlop
92 | self.animationDuration = animationDuration
93 | self.onPageTranslationChanged = onPageTranslationChanged
94 | }
95 |
96 | private func calculateDisplayItems(originalIndex: Int) {
97 | displayedItems.removeAll()
98 |
99 | var start = originalIndex - 1
100 | if start < 0 {
101 | start = 0
102 | }
103 | var end = start + offscreenCountPerSide * 2
104 | if end >= items.count {
105 | end = items.count - 1
106 | }
107 | for i in start...end {
108 | if let index = items.index(items.startIndex, offsetBy: i, limitedBy: items.endIndex) {
109 | displayedItems.append(items[index])
110 | }
111 | }
112 | }
113 |
114 | public var body: some View {
115 | #if !os(tvOS)
116 | // Note that `minimumDistance` seems not work in real iPhone & iPad in iOS 16.2.
117 | // We use the default value of `minimumDistance` to prevent gesture conflict.
118 | let gesture = DragGesture(coordinateSpace: .global)
119 | .onEnded { value in
120 | onGestureEnd(value: value)
121 | }
122 | .updating($dragTranslationX) { v, state, _ in
123 | state = v.translation.width
124 | }
125 | #endif
126 |
127 | GeometryReader { proxy in
128 | HStack(spacing: spacing) {
129 | ForEach(displayedItems, id: \.id) { item in
130 | itemContent(item)
131 | .frame(width: proxy.size.width, height: proxy.size.height)
132 | }
133 | }
134 | .frame(maxWidth: .infinity, maxHeight: .infinity)
135 | .offset(x: (-CGFloat(virtualPageIndex) * (proxy.size.width + spacing)) + translationX)
136 | }
137 | .contentShape(Rectangle())
138 | #if !os(tvOS)
139 | .simultaneousGesture(gesture, including: disablePaging.wrappedValue ? .subviews : .all)
140 | #endif
141 | .onAppear {
142 | calculateDisplayItems(originalIndex: pageIndex.wrappedValue)
143 | updateVirtualPageIndex(originalIndex: pageIndex.wrappedValue)
144 | }
145 | .onChange(of: dragTranslationX) { newValue in
146 | translationX = newValue
147 |
148 | let progress: CGFloat = abs(translationX) / self.width
149 | if progress == 0 {
150 | return
151 | }
152 |
153 | let currentIndex = pageIndex.wrappedValue
154 | var nextIndex = translationX < 0 ? currentIndex + 1 : currentIndex - 1
155 | nextIndex = nextIndex.clamp(to: 0...items.count - 1)
156 |
157 | let translation = PagingTranslation(currentIndex: currentIndex,
158 | nextIndex: nextIndex, progress: progress)
159 | self.onPageTranslationChanged?(translation)
160 | }
161 | .onChange(of: pageIndex.wrappedValue, perform: { newValue in
162 | withEastOutAnimation(duration: animationDuration) {
163 | calculateDisplayItems(originalIndex: newValue)
164 | updateVirtualPageIndex(originalIndex: newValue)
165 | }
166 | })
167 | .listenWidthChanged { width in
168 | self.width = width
169 | }
170 | .id(items)
171 | }
172 |
173 | #if !os(tvOS)
174 | private func onGestureEnd(value: DragGesture.Value) {
175 | withEastOutAnimation(duration: animationDuration) {
176 | var newVirtualIndex = virtualPageIndex
177 | if translationX > scrollSlop {
178 | var index = virtualPageIndex - 1
179 | if index < 0 {
180 | index = 0
181 | }
182 | newVirtualIndex = index
183 | } else if translationX < -scrollSlop {
184 | var index = virtualPageIndex + 1
185 | if index >= CGFloat(displayedItems.count) {
186 | index = CGFloat(displayedItems.count) - 1
187 | }
188 | newVirtualIndex = index
189 | }
190 |
191 | self.virtualPageIndex = newVirtualIndex
192 | self.translationX = 0
193 |
194 | let currentItem = displayedItems[Int(self.virtualPageIndex)]
195 | let nextOriginalIndex = items.firstIndex { id in
196 | id == currentItem
197 | } as! Int
198 |
199 | let translation = PagingTranslation(currentIndex: pageIndex.wrappedValue,
200 | nextIndex: nextOriginalIndex, progress: 1.0)
201 |
202 | self.onPageTranslationChanged?(translation)
203 |
204 | // TODO Use new withAnimation API in iOS 17/macOS 14 to get the exact moment when the animation ends.
205 | DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
206 | withEastOutAnimation(duration: animationDuration) {
207 | calculateDisplayItems(originalIndex: nextOriginalIndex)
208 | updateVirtualPageIndex(originalIndex: nextOriginalIndex)
209 | }
210 | }
211 | }
212 | }
213 | #endif
214 |
215 | private func updateVirtualPageIndex(originalIndex: Int) {
216 | if originalIndex != 0 && originalIndex != items.count - 1 {
217 | virtualPageIndex = 1
218 | } else if originalIndex == 0 {
219 | virtualPageIndex = 0
220 | } else if originalIndex == items.count - 1 {
221 | virtualPageIndex = CGFloat(displayedItems.count) - 1
222 | }
223 |
224 | displayedItemId = displayedItems[Int(virtualPageIndex)].id
225 | }
226 | }
227 |
228 | fileprivate func withEastOutAnimation(duration: Double = 0.3,
229 | _ delay: Double = 0.0,
230 | _ body: () throws -> Result) -> Result? {
231 | return try? withAnimation(Animation.easeOut(duration: duration).delay(delay)) {
232 | try body()
233 | }
234 | }
235 |
236 | fileprivate extension View {
237 | /// Listen the width changed of this view.
238 | /// - Parameter onWidthChanged: invoked on width changed
239 | func listenWidthChanged(onWidthChanged: @escaping (CGFloat) -> Void) -> some View {
240 | self.overlay(GeometryReader(content: { proxy in
241 | Color.clear.onChange(of: proxy.size.width) { newValue in
242 | onWidthChanged(newValue)
243 | }.onAppear {
244 | onWidthChanged(proxy.size.width)
245 | }
246 | }))
247 | }
248 | }
249 |
250 | fileprivate extension Comparable {
251 | func clamp(to range: ClosedRange) -> Self {
252 | if self < range.lowerBound {
253 | return range.lowerBound
254 | }
255 | if self > range.upperBound {
256 | return range.upperBound
257 | }
258 | return self
259 | }
260 | }
261 |
--------------------------------------------------------------------------------
/PageViewSample/PageViewSample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 224EF17D2A4BF5CE00035CA4 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = 224EF17C2A4BF5CE00035CA4 /* Nuke */; };
11 | 224EF17F2A4BF5CE00035CA4 /* NukeExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 224EF17E2A4BF5CE00035CA4 /* NukeExtensions */; };
12 | 224EF1812A4BF5CE00035CA4 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 224EF1802A4BF5CE00035CA4 /* NukeUI */; };
13 | 22F6ECFF2969A146002D0A16 /* PageViewSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F6ECFE2969A146002D0A16 /* PageViewSampleApp.swift */; };
14 | 22F6ED012969A146002D0A16 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22F6ED002969A146002D0A16 /* ContentView.swift */; };
15 | 22F6ED032969A146002D0A16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22F6ED022969A146002D0A16 /* Assets.xcassets */; };
16 | 22F6ED072969A146002D0A16 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22F6ED062969A146002D0A16 /* Preview Assets.xcassets */; };
17 | 22F6ED132969A2CA002D0A16 /* PageView in Frameworks */ = {isa = PBXBuildFile; productRef = 22F6ED122969A2CA002D0A16 /* PageView */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 221D91022969AAF600CB7A44 /* PageView.SwiftUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = PageView.SwiftUI; path = ..; sourceTree = ""; };
22 | 22F6ECFB2969A146002D0A16 /* PageViewSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PageViewSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
23 | 22F6ECFE2969A146002D0A16 /* PageViewSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewSampleApp.swift; sourceTree = ""; };
24 | 22F6ED002969A146002D0A16 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
25 | 22F6ED022969A146002D0A16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
26 | 22F6ED042969A146002D0A16 /* PageViewSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PageViewSample.entitlements; sourceTree = ""; };
27 | 22F6ED062969A146002D0A16 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
28 | /* End PBXFileReference section */
29 |
30 | /* Begin PBXFrameworksBuildPhase section */
31 | 22F6ECF82969A146002D0A16 /* Frameworks */ = {
32 | isa = PBXFrameworksBuildPhase;
33 | buildActionMask = 2147483647;
34 | files = (
35 | 22F6ED132969A2CA002D0A16 /* PageView in Frameworks */,
36 | 224EF17D2A4BF5CE00035CA4 /* Nuke in Frameworks */,
37 | 224EF17F2A4BF5CE00035CA4 /* NukeExtensions in Frameworks */,
38 | 224EF1812A4BF5CE00035CA4 /* NukeUI in Frameworks */,
39 | );
40 | runOnlyForDeploymentPostprocessing = 0;
41 | };
42 | /* End PBXFrameworksBuildPhase section */
43 |
44 | /* Begin PBXGroup section */
45 | 22F6ECF22969A145002D0A16 = {
46 | isa = PBXGroup;
47 | children = (
48 | 221D91022969AAF600CB7A44 /* PageView.SwiftUI */,
49 | 22F6ECFD2969A146002D0A16 /* PageViewSample */,
50 | 22F6ECFC2969A146002D0A16 /* Products */,
51 | 22F6ED0E2969A193002D0A16 /* Frameworks */,
52 | );
53 | sourceTree = "";
54 | };
55 | 22F6ECFC2969A146002D0A16 /* Products */ = {
56 | isa = PBXGroup;
57 | children = (
58 | 22F6ECFB2969A146002D0A16 /* PageViewSample.app */,
59 | );
60 | name = Products;
61 | sourceTree = "";
62 | };
63 | 22F6ECFD2969A146002D0A16 /* PageViewSample */ = {
64 | isa = PBXGroup;
65 | children = (
66 | 22F6ECFE2969A146002D0A16 /* PageViewSampleApp.swift */,
67 | 22F6ED002969A146002D0A16 /* ContentView.swift */,
68 | 22F6ED022969A146002D0A16 /* Assets.xcassets */,
69 | 22F6ED042969A146002D0A16 /* PageViewSample.entitlements */,
70 | 22F6ED052969A146002D0A16 /* Preview Content */,
71 | );
72 | path = PageViewSample;
73 | sourceTree = "";
74 | };
75 | 22F6ED052969A146002D0A16 /* Preview Content */ = {
76 | isa = PBXGroup;
77 | children = (
78 | 22F6ED062969A146002D0A16 /* Preview Assets.xcassets */,
79 | );
80 | path = "Preview Content";
81 | sourceTree = "";
82 | };
83 | 22F6ED0E2969A193002D0A16 /* Frameworks */ = {
84 | isa = PBXGroup;
85 | children = (
86 | );
87 | name = Frameworks;
88 | sourceTree = "";
89 | };
90 | /* End PBXGroup section */
91 |
92 | /* Begin PBXNativeTarget section */
93 | 22F6ECFA2969A146002D0A16 /* PageViewSample */ = {
94 | isa = PBXNativeTarget;
95 | buildConfigurationList = 22F6ED0A2969A146002D0A16 /* Build configuration list for PBXNativeTarget "PageViewSample" */;
96 | buildPhases = (
97 | 22F6ECF72969A146002D0A16 /* Sources */,
98 | 22F6ECF82969A146002D0A16 /* Frameworks */,
99 | 22F6ECF92969A146002D0A16 /* Resources */,
100 | );
101 | buildRules = (
102 | );
103 | dependencies = (
104 | );
105 | name = PageViewSample;
106 | packageProductDependencies = (
107 | 22F6ED122969A2CA002D0A16 /* PageView */,
108 | 224EF17C2A4BF5CE00035CA4 /* Nuke */,
109 | 224EF17E2A4BF5CE00035CA4 /* NukeExtensions */,
110 | 224EF1802A4BF5CE00035CA4 /* NukeUI */,
111 | );
112 | productName = PageViewSample;
113 | productReference = 22F6ECFB2969A146002D0A16 /* PageViewSample.app */;
114 | productType = "com.apple.product-type.application";
115 | };
116 | /* End PBXNativeTarget section */
117 |
118 | /* Begin PBXProject section */
119 | 22F6ECF32969A145002D0A16 /* Project object */ = {
120 | isa = PBXProject;
121 | attributes = {
122 | BuildIndependentTargetsInParallel = 1;
123 | LastSwiftUpdateCheck = 1420;
124 | LastUpgradeCheck = 1420;
125 | TargetAttributes = {
126 | 22F6ECFA2969A146002D0A16 = {
127 | CreatedOnToolsVersion = 14.2;
128 | };
129 | };
130 | };
131 | buildConfigurationList = 22F6ECF62969A145002D0A16 /* Build configuration list for PBXProject "PageViewSample" */;
132 | compatibilityVersion = "Xcode 14.0";
133 | developmentRegion = en;
134 | hasScannedForEncodings = 0;
135 | knownRegions = (
136 | en,
137 | Base,
138 | );
139 | mainGroup = 22F6ECF22969A145002D0A16;
140 | packageReferences = (
141 | 22F6ED112969A2CA002D0A16 /* XCRemoteSwiftPackageReference "PageView" */,
142 | 224EF17B2A4BF5CE00035CA4 /* XCRemoteSwiftPackageReference "Nuke" */,
143 | );
144 | productRefGroup = 22F6ECFC2969A146002D0A16 /* Products */;
145 | projectDirPath = "";
146 | projectRoot = "";
147 | targets = (
148 | 22F6ECFA2969A146002D0A16 /* PageViewSample */,
149 | );
150 | };
151 | /* End PBXProject section */
152 |
153 | /* Begin PBXResourcesBuildPhase section */
154 | 22F6ECF92969A146002D0A16 /* Resources */ = {
155 | isa = PBXResourcesBuildPhase;
156 | buildActionMask = 2147483647;
157 | files = (
158 | 22F6ED072969A146002D0A16 /* Preview Assets.xcassets in Resources */,
159 | 22F6ED032969A146002D0A16 /* Assets.xcassets in Resources */,
160 | );
161 | runOnlyForDeploymentPostprocessing = 0;
162 | };
163 | /* End PBXResourcesBuildPhase section */
164 |
165 | /* Begin PBXSourcesBuildPhase section */
166 | 22F6ECF72969A146002D0A16 /* Sources */ = {
167 | isa = PBXSourcesBuildPhase;
168 | buildActionMask = 2147483647;
169 | files = (
170 | 22F6ED012969A146002D0A16 /* ContentView.swift in Sources */,
171 | 22F6ECFF2969A146002D0A16 /* PageViewSampleApp.swift in Sources */,
172 | );
173 | runOnlyForDeploymentPostprocessing = 0;
174 | };
175 | /* End PBXSourcesBuildPhase section */
176 |
177 | /* Begin XCBuildConfiguration section */
178 | 22F6ED082969A146002D0A16 /* Debug */ = {
179 | isa = XCBuildConfiguration;
180 | buildSettings = {
181 | ALWAYS_SEARCH_USER_PATHS = NO;
182 | CLANG_ANALYZER_NONNULL = YES;
183 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
184 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
185 | CLANG_ENABLE_MODULES = YES;
186 | CLANG_ENABLE_OBJC_ARC = YES;
187 | CLANG_ENABLE_OBJC_WEAK = YES;
188 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
189 | CLANG_WARN_BOOL_CONVERSION = YES;
190 | CLANG_WARN_COMMA = YES;
191 | CLANG_WARN_CONSTANT_CONVERSION = YES;
192 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
194 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
195 | CLANG_WARN_EMPTY_BODY = YES;
196 | CLANG_WARN_ENUM_CONVERSION = YES;
197 | CLANG_WARN_INFINITE_RECURSION = YES;
198 | CLANG_WARN_INT_CONVERSION = YES;
199 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
200 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
201 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
202 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
203 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
204 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
205 | CLANG_WARN_STRICT_PROTOTYPES = YES;
206 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
207 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
208 | CLANG_WARN_UNREACHABLE_CODE = YES;
209 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
210 | COPY_PHASE_STRIP = NO;
211 | DEBUG_INFORMATION_FORMAT = dwarf;
212 | ENABLE_STRICT_OBJC_MSGSEND = YES;
213 | ENABLE_TESTABILITY = YES;
214 | GCC_C_LANGUAGE_STANDARD = gnu11;
215 | GCC_DYNAMIC_NO_PIC = NO;
216 | GCC_NO_COMMON_BLOCKS = YES;
217 | GCC_OPTIMIZATION_LEVEL = 0;
218 | GCC_PREPROCESSOR_DEFINITIONS = (
219 | "DEBUG=1",
220 | "$(inherited)",
221 | );
222 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
223 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
224 | GCC_WARN_UNDECLARED_SELECTOR = YES;
225 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
226 | GCC_WARN_UNUSED_FUNCTION = YES;
227 | GCC_WARN_UNUSED_VARIABLE = YES;
228 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
229 | MTL_FAST_MATH = YES;
230 | ONLY_ACTIVE_ARCH = YES;
231 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
232 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
233 | };
234 | name = Debug;
235 | };
236 | 22F6ED092969A146002D0A16 /* Release */ = {
237 | isa = XCBuildConfiguration;
238 | buildSettings = {
239 | ALWAYS_SEARCH_USER_PATHS = NO;
240 | CLANG_ANALYZER_NONNULL = YES;
241 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
242 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
243 | CLANG_ENABLE_MODULES = YES;
244 | CLANG_ENABLE_OBJC_ARC = YES;
245 | CLANG_ENABLE_OBJC_WEAK = YES;
246 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
247 | CLANG_WARN_BOOL_CONVERSION = YES;
248 | CLANG_WARN_COMMA = YES;
249 | CLANG_WARN_CONSTANT_CONVERSION = YES;
250 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
251 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
252 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
253 | CLANG_WARN_EMPTY_BODY = YES;
254 | CLANG_WARN_ENUM_CONVERSION = YES;
255 | CLANG_WARN_INFINITE_RECURSION = YES;
256 | CLANG_WARN_INT_CONVERSION = YES;
257 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
258 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
259 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
261 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
262 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
263 | CLANG_WARN_STRICT_PROTOTYPES = YES;
264 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
265 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
266 | CLANG_WARN_UNREACHABLE_CODE = YES;
267 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
268 | COPY_PHASE_STRIP = NO;
269 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
270 | ENABLE_NS_ASSERTIONS = NO;
271 | ENABLE_STRICT_OBJC_MSGSEND = YES;
272 | GCC_C_LANGUAGE_STANDARD = gnu11;
273 | GCC_NO_COMMON_BLOCKS = YES;
274 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
275 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
276 | GCC_WARN_UNDECLARED_SELECTOR = YES;
277 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
278 | GCC_WARN_UNUSED_FUNCTION = YES;
279 | GCC_WARN_UNUSED_VARIABLE = YES;
280 | MTL_ENABLE_DEBUG_INFO = NO;
281 | MTL_FAST_MATH = YES;
282 | SWIFT_COMPILATION_MODE = wholemodule;
283 | SWIFT_OPTIMIZATION_LEVEL = "-O";
284 | };
285 | name = Release;
286 | };
287 | 22F6ED0B2969A146002D0A16 /* Debug */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
291 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
292 | CODE_SIGN_ENTITLEMENTS = PageViewSample/PageViewSample.entitlements;
293 | CODE_SIGN_STYLE = Automatic;
294 | CURRENT_PROJECT_VERSION = 1;
295 | DEVELOPMENT_ASSET_PATHS = "\"PageViewSample/Preview Content\"";
296 | DEVELOPMENT_TEAM = 7GB9CHCB87;
297 | ENABLE_HARDENED_RUNTIME = YES;
298 | ENABLE_PREVIEWS = YES;
299 | GENERATE_INFOPLIST_FILE = YES;
300 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
301 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
302 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
303 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
304 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
305 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
306 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
307 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
308 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
309 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
310 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
311 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
312 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
313 | MACOSX_DEPLOYMENT_TARGET = 12.0;
314 | MARKETING_VERSION = 1.0;
315 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.PageViewSample;
316 | PRODUCT_NAME = "$(TARGET_NAME)";
317 | SDKROOT = auto;
318 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
319 | SWIFT_EMIT_LOC_STRINGS = YES;
320 | SWIFT_VERSION = 5.0;
321 | TARGETED_DEVICE_FAMILY = "1,2";
322 | };
323 | name = Debug;
324 | };
325 | 22F6ED0C2969A146002D0A16 /* Release */ = {
326 | isa = XCBuildConfiguration;
327 | buildSettings = {
328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
329 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
330 | CODE_SIGN_ENTITLEMENTS = PageViewSample/PageViewSample.entitlements;
331 | CODE_SIGN_STYLE = Automatic;
332 | CURRENT_PROJECT_VERSION = 1;
333 | DEVELOPMENT_ASSET_PATHS = "\"PageViewSample/Preview Content\"";
334 | DEVELOPMENT_TEAM = 7GB9CHCB87;
335 | ENABLE_HARDENED_RUNTIME = YES;
336 | ENABLE_PREVIEWS = YES;
337 | GENERATE_INFOPLIST_FILE = YES;
338 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
339 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
340 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
341 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
342 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
343 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
344 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
345 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
346 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
347 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
348 | IPHONEOS_DEPLOYMENT_TARGET = 15.0;
349 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
350 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
351 | MACOSX_DEPLOYMENT_TARGET = 12.0;
352 | MARKETING_VERSION = 1.0;
353 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.PageViewSample;
354 | PRODUCT_NAME = "$(TARGET_NAME)";
355 | SDKROOT = auto;
356 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
357 | SWIFT_EMIT_LOC_STRINGS = YES;
358 | SWIFT_VERSION = 5.0;
359 | TARGETED_DEVICE_FAMILY = "1,2";
360 | };
361 | name = Release;
362 | };
363 | /* End XCBuildConfiguration section */
364 |
365 | /* Begin XCConfigurationList section */
366 | 22F6ECF62969A145002D0A16 /* Build configuration list for PBXProject "PageViewSample" */ = {
367 | isa = XCConfigurationList;
368 | buildConfigurations = (
369 | 22F6ED082969A146002D0A16 /* Debug */,
370 | 22F6ED092969A146002D0A16 /* Release */,
371 | );
372 | defaultConfigurationIsVisible = 0;
373 | defaultConfigurationName = Release;
374 | };
375 | 22F6ED0A2969A146002D0A16 /* Build configuration list for PBXNativeTarget "PageViewSample" */ = {
376 | isa = XCConfigurationList;
377 | buildConfigurations = (
378 | 22F6ED0B2969A146002D0A16 /* Debug */,
379 | 22F6ED0C2969A146002D0A16 /* Release */,
380 | );
381 | defaultConfigurationIsVisible = 0;
382 | defaultConfigurationName = Release;
383 | };
384 | /* End XCConfigurationList section */
385 |
386 | /* Begin XCRemoteSwiftPackageReference section */
387 | 224EF17B2A4BF5CE00035CA4 /* XCRemoteSwiftPackageReference "Nuke" */ = {
388 | isa = XCRemoteSwiftPackageReference;
389 | repositoryURL = "https://github.com/kean/Nuke";
390 | requirement = {
391 | kind = upToNextMajorVersion;
392 | minimumVersion = 12.0.0;
393 | };
394 | };
395 | 22F6ED112969A2CA002D0A16 /* XCRemoteSwiftPackageReference "PageView" */ = {
396 | isa = XCRemoteSwiftPackageReference;
397 | repositoryURL = "https://github.com/JuniperPhoton/PageView.SwiftUI";
398 | requirement = {
399 | branch = main;
400 | kind = branch;
401 | };
402 | };
403 | /* End XCRemoteSwiftPackageReference section */
404 |
405 | /* Begin XCSwiftPackageProductDependency section */
406 | 224EF17C2A4BF5CE00035CA4 /* Nuke */ = {
407 | isa = XCSwiftPackageProductDependency;
408 | package = 224EF17B2A4BF5CE00035CA4 /* XCRemoteSwiftPackageReference "Nuke" */;
409 | productName = Nuke;
410 | };
411 | 224EF17E2A4BF5CE00035CA4 /* NukeExtensions */ = {
412 | isa = XCSwiftPackageProductDependency;
413 | package = 224EF17B2A4BF5CE00035CA4 /* XCRemoteSwiftPackageReference "Nuke" */;
414 | productName = NukeExtensions;
415 | };
416 | 224EF1802A4BF5CE00035CA4 /* NukeUI */ = {
417 | isa = XCSwiftPackageProductDependency;
418 | package = 224EF17B2A4BF5CE00035CA4 /* XCRemoteSwiftPackageReference "Nuke" */;
419 | productName = NukeUI;
420 | };
421 | 22F6ED122969A2CA002D0A16 /* PageView */ = {
422 | isa = XCSwiftPackageProductDependency;
423 | package = 22F6ED112969A2CA002D0A16 /* XCRemoteSwiftPackageReference "PageView" */;
424 | productName = PageView;
425 | };
426 | /* End XCSwiftPackageProductDependency section */
427 | };
428 | rootObject = 22F6ECF32969A145002D0A16 /* Project object */;
429 | }
430 |
--------------------------------------------------------------------------------