Skip to content

Instantly share code, notes, and snippets.

@beader
Last active October 4, 2024 08:17
Show Gist options
  • Save beader/08757070b8c8b1134ea8e53f347553d8 to your computer and use it in GitHub Desktop.
Save beader/08757070b8c8b1134ea8e53f347553d8 to your computer and use it in GitHub Desktop.
Create a InfinitePageView for SwiftUI using UIViewControllerRepresentable by the help of ChatGPT

InfinitePageView for SwiftUI

Create a Bi-directional Infinite PageView for SwiftUI using UIViewControllerRepresentable & UIPageViewController by the help of ChatGPT.

Checkout the video demo in the comment.

The code is generated by ChatGPT through multiple rounds of conversations. Checkout the conversations between ChatGPT and me if you are interested in.

//
// PagedInfiniteScrollView.swift
// InfinitePageView
//
// Created by beader on 2023/4/19.
//
import SwiftUI
import UIKit
struct PagedInfiniteScrollView<S: Steppable & Comparable, Content: View>: UIViewControllerRepresentable {
typealias UIViewControllerType = UIPageViewController
let content: (S) -> Content
@Binding var currentPage: S
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
let initialViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)
return pageViewController
}
func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
let currentViewController = uiViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content, S>>
let currentIndex = currentViewController?.rootView.index ?? .origin
if currentPage != currentIndex {
let direction: UIPageViewController.NavigationDirection = currentPage > currentIndex ? .forward : .reverse
let newViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
uiViewController.setViewControllers([newViewController], direction: direction, animated: true, completion: nil)
}
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PagedInfiniteScrollView
init(_ parent: PagedInfiniteScrollView) {
self.parent = parent
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content, S>>, let currentIndex = currentView.rootView.index as S? else {
return nil
}
let previousIndex = currentIndex.backward()
return UIHostingController(rootView: IdentifiableContent(index: previousIndex, content: { parent.content(previousIndex) }))
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content, S>>, let currentIndex = currentView.rootView.index as S? else {
return nil
}
let nextIndex = currentIndex.forward()
return UIHostingController(rootView: IdentifiableContent(index: nextIndex, content: { parent.content(nextIndex) }))
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let currentView = pageViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content, S>>,
let currentIndex = currentView.rootView.index as S? {
parent.currentPage = currentIndex
}
}
}
}
extension PagedInfiniteScrollView {
struct IdentifiableContent<Content: View, S: Steppable>: View {
let index: S
let content: Content
init(index: S, @ViewBuilder content: () -> Content) {
self.index = index
self.content = content()
}
var body: some View {
content
}
}
}
//
// Steppable.swift
// InfinitePageView
//
// Created by beader on 2023/4/20.
//
import Foundation
protocol Steppable {
static var origin: Self { get }
func forward() -> Self
func backward() -> Self
}
extension Int: Steppable {
static var origin: Int {
return 0
}
func forward() -> Int {
return self + 1
}
func backward() -> Int {
return self - 1
}
}
extension Date: Steppable {
static var origin: Date {
return .now
}
func forward() -> Date {
return Calendar.current.date(byAdding: .day, value: 1, to: self) ?? self
}
func backward() -> Date {
return Calendar.current.date(byAdding: .day, value: -1, to: self) ?? self
}
}
struct ContentView: View {
@State private var currentPage: Date = .now
var body: some View {
VStack {
PagedInfiniteScrollView(content: { index in
Text("\(index.formatted(date: .complete, time: .omitted))")
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.edgesIgnoringSafeArea(.all)
}, currentPage: $currentPage)
HStack {
Button(action: {
currentPage -= 1
}) {
Text("Previous")
}
.padding()
.background(Color.white)
.cornerRadius(8)
Button(action: {
currentPage += 1
}) {
Text("Next")
}
.padding()
.background(Color.white)
.cornerRadius(8)
}
.padding(.bottom, 16)
}
}
}

Model: GPT-4

2 / 2

Implement a horizontal paged infinite scrollview in swiftui or using uirepresentable

ChatGPTChatGPT

1 / 1

In SwiftUI, you can create a horizontal paged infinite ScrollView using UIPageViewController and UIViewControllerRepresentable. Here's a sample implementation:

  1. First, create a SwiftUI view that you want to display in the horizontal paged infinite ScrollView:

swift

import SwiftUI

struct PageView: View {
    let index: Int

    var body: some View {
        VStack {
            Text("Page \(index)")
                .font(.largeTitle)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue)
        .edgesIgnoringSafeArea(.all)
    }
}
  1. Next, create a UIPageViewController wrapper using UIViewControllerRepresentable:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator
        
        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? HostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let previousIndex = currentIndex - 1

            return UIHostingController(rootView: PageView(index: previousIndex))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? HostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let nextIndex = currentIndex + 1

            return UIHostingController(rootView: PageView(index: nextIndex))
        }
    }
}
  1. Finally, use the PagedInfiniteScrollView in your main view:

swift

import SwiftUI

struct ContentView: View {
    var body: some View {
        PagedInfiniteScrollView()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This implementation creates a horizontal paged infinite ScrollView that can scroll in both directions infinitely, and it uses the PageView as the content of each page.

1 / 1

Cannot find type 'HostingController' in scope

ChatGPTChatGPT

1 / 1

Apologies for the confusion. It looks like I made a mistake in my previous response. You should use UIHostingController instead of HostingController. Here's the updated Coordinator class in the PagedInfiniteScrollView:

swift

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    var parent: PagedInfiniteScrollView

    init(_ parent: PagedInfiniteScrollView) {
        self.parent = parent
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let currentView = viewController as? UIHostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
            return nil
        }

        let previousIndex = currentIndex - 1

        return UIHostingController(rootView: PageView(index: previousIndex))
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let currentView = viewController as? UIHostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
            return nil
        }

        let nextIndex = currentIndex + 1

        return UIHostingController(rootView: PageView(index: nextIndex))
    }
}

With this change, you should be able to use the PagedInfiniteScrollView without any issues.

1 / 1

The code can compiled but I saw nothing on the screen

ChatGPTChatGPT

1 / 1

Apologies for the oversight. It seems I missed setting the initial view controller for the UIPageViewController. Here's the updated version of the PagedInfiniteScrollView with the initial view controller set:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        let initialViewController = UIHostingController(rootView: PageView(index: 0))
        pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)

        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let previousIndex = currentIndex - 1

            return UIHostingController(rootView: PageView(index: previousIndex))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<PageView>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let nextIndex = currentIndex + 1

            return UIHostingController(rootView: PageView(index: nextIndex))
        }
    }
}

Now, when you run the code, you should see the PageView with index 0 on the screen, and you can swipe left or right to navigate between the pages.

3 / 3

It works now. Thanks! I want PagedInfiniteScrollView accept a content argument which is a view builder: @ViewBuilder content: @escaping (Int) -> Content. So I can use PagedInfiniteScrollView like this: ``` PagedInfiniteScrollView { index in Text("\(index)") } ```

ChatGPTChatGPT

1 / 1

To achieve that, you can modify the PagedInfiniteScrollView to accept a @ViewBuilder closure. Here's the updated implementation:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView<Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    let content: (Int) -> Content

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        let initialViewController = UIHostingController(rootView: content(0))
        pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)

        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<Content>, let currentIndex = currentView.rootView.content.index as Int? else {
                return nil
            }

            let previousIndex = currentIndex - 1

            return UIHostingController(rootView: parent.content(previousIndex))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<Content>, let currentIndex = currentView.rootView.content.index as Int? else {
                return nil
            }

            let nextIndex = currentIndex + 1

            return UIHostingController(rootView: parent.content(nextIndex))
        }
    }
}

Now you can use the PagedInfiniteScrollView with a custom content view like this:

swift

struct ContentView: View {
    var body: some View {
        PagedInfiniteScrollView { index in
            Text("\(index)")
                .font(.largeTitle)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.blue)
                .edgesIgnoringSafeArea(.all)
        }
    }
}

However, there's an issue with this implementation. The code tries to access the index from the content view, which is not available. To fix this, you can create a IdentifiableContent view that wraps your content view and holds the index:

swift

struct IdentifiableContent<Content: View>: View {
    let index: Int
    let content: Content

    init(index: Int, @ViewBuilder content: () -> Content) {
        self.index = index
        self.content = content()
    }

    var body: some View {
        content
    }
}

Then, update the PagedInfiniteScrollView to use the IdentifiableContent view:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView<Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    let content: (Int) -> Content

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.co

1 / 1

Value of type 'Content' has no member 'content'

ChatGPTChatGPT

1 / 1

Apologies for the confusion. I made a mistake in my previous response. We need to update the Coordinator class to handle the IdentifiableContent view properly. Here's the updated PagedInfiniteScrollView with the corrected Coordinator class:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView<Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    let content: (Int) -> Content

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        let initialViewController = UIHostingController(rootView: IdentifiableContent(index: 0, content: { content(0) }))
        pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)

        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content>>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let previousIndex = currentIndex - 1

            return UIHostingController(rootView: IdentifiableContent(index: previousIndex, content: { parent.content(previousIndex) }))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content>>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let nextIndex = currentIndex + 1

            return UIHostingController(rootView: IdentifiableContent(index: nextIndex, content: { parent.content(nextIndex) }))
        }
    }
}

Now, you should be able to use the PagedInfiniteScrollView with a custom content view as shown in the previous example:

swift

struct ContentView: View {
    var body: some View {
        PagedInfiniteScrollView { index in
            Text("\(index)")
                .font(.largeTitle)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.blue)
                .edgesIgnoringSafeArea(.all)
        }
    }
}

1 / 1

Cannot find 'IdentifiableContent' in scope

ChatGPTChatGPT

1 / 1

Apologies for the confusion. It appears I missed including the IdentifiableContent view in my previous response. Here's the IdentifiableContent view code:

swift

struct IdentifiableContent<Content: View>: View {
    let index: Int
    let content: Content

    init(index: Int, @ViewBuilder content: () -> Content) {
        self.index = index
        self.content = content()
    }

    var body: some View {
        content
    }
}

Include the IdentifiableContent view in your project, and the PagedInfiniteScrollView should work as expected with the custom content view.

1 / 1

It works great. I want to add a page index binding so I can move to target page outside.

ChatGPTChatGPT

1 / 1

To add a page index binding to the PagedInfiniteScrollView, you can modify the struct to accept a Binding for the current page index. You'll also need to update the Coordinator class to observe changes to the current page index and update the view accordingly. Here's the updated implementation:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView<Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    let content: (Int) -> Content
    @Binding var currentPage: Int

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        let initialViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
        pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)

        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
        let currentViewController = uiViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content>>
        let currentIndex = currentViewController?.rootView.index ?? 0

        if currentPage != currentIndex {
            let direction: UIPageViewController.NavigationDirection = currentPage > currentIndex ? .forward : .reverse
            let newViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
            uiViewController.setViewControllers([newViewController], direction: direction, animated: true, completion: nil)
        }
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content>>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let previousIndex = currentIndex - 1

            return UIHostingController(rootView: IdentifiableContent(index: previousIndex, content: { parent.content(previousIndex) }))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content>>, let currentIndex = currentView.rootView.index as Int? else {
                return nil
            }

            let nextIndex = currentIndex + 1

            return UIHostingController(rootView: IdentifiableContent(index: nextIndex, content: { parent.content(nextIndex) }))
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
               let currentView = pageViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content>>,
               let currentIndex = currentView.rootView.index as Int? {
                parent.currentPage = currentIndex
            }
        }
    }
}

Now, you can use the PagedInfiniteScrollView with a custom content view and a binding for the current page index:

swift

struct ContentView: View {
    @State private var currentPage: Int = 0

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { index in
                Text("\(index)")

1 / 1

Continue from struct ContentView: View {

ChatGPTChatGPT

1 / 1

Apologies for the truncation. Here's the complete example of using PagedInfiniteScrollView with a binding for the current page index:

swift

struct ContentView: View {
    @State private var currentPage: Int = 0

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { index in
                Text("\(index)")
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .edgesIgnoringSafeArea(.all)
            }, currentPage: $currentPage)

            HStack {
                Button(action: {
                    if currentPage > 0 {
                        currentPage -= 1
                    }
                }) {
                    Text("Previous")
                }
                .padding()
                .background(Color.white)
                .cornerRadius(8)

                Button(action: {
                    currentPage += 1
                }) {
                    Text("Next")
                }
                .padding()
                .background(Color.white)
                .cornerRadius(8)
            }
            .padding(.bottom, 16)
        }
    }
}

In this example, I've added a VStack containing the PagedInfiniteScrollView and an HStack with two buttons for navigating between pages. The buttons will update the currentPage state property, which in turn will cause the PagedInfiniteScrollView to update and navigate to the target page.

1 / 1

It works great. I want to create a protocol named Steppable which has a forward and backward method. Then I can make Int and Date conforms to Steppable. Call forward to a date I can get the next day. Then modify the PagedInfiniteScrollView which accept a Stoppable generic for the index. The content argument now is (Stoppable) -> Content

ChatGPTChatGPT

1 / 1

To implement this, first create the Steppable protocol with forward() and backward() methods. Then, make Int and Date conform to the Steppable protocol. Finally, update the PagedInfiniteScrollView to accept a Steppable generic for the index. Here's the complete solution:

  1. Create the Steppable protocol:

swift

protocol Steppable {
    func forward() -> Self
    func backward() -> Self
}
  1. Make Int and Date conform to the Steppable protocol:

swift

extension Int: Steppable {
    func forward() -> Int {
        return self + 1
    }

    func backward() -> Int {
        return self - 1
    }
}

extension Date: Steppable {
    func forward() -> Date {
        return Calendar.current.date(byAdding: .day, value: 1, to: self) ?? self
    }

    func backward() -> Date {
        return Calendar.current.date(byAdding: .day, value: -1, to: self) ?? self
    }
}
  1. Modify the PagedInfiniteScrollView to accept a Steppable generic for the index:

swift

import SwiftUI
import UIKit

struct PagedInfiniteScrollView<S: Steppable, Content: View>: UIViewControllerRepresentable {
    typealias UIViewControllerType = UIPageViewController

    let content: (S) -> Content
    @Binding var currentPage: S

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        let initialViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
        pageViewController.setViewControllers([initialViewController], direction: .forward, animated: false, completion: nil)

        return pageViewController
    }

    func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
        let currentViewController = uiViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content, S>>
        let currentIndex = currentViewController?.rootView.index ?? currentPage

        if !currentPage.isEqual(to: currentIndex) {
            let direction: UIPageViewController.NavigationDirection = currentPage.isGreaterThan(currentIndex) ? .forward : .reverse
            let newViewController = UIHostingController(rootView: IdentifiableContent(index: currentPage, content: { content(currentPage) }))
            uiViewController.setViewControllers([newViewController], direction: direction, animated: true, completion: nil)
        }
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PagedInfiniteScrollView

        init(_ parent: PagedInfiniteScrollView) {
            self.parent = parent
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content, S>>, let currentIndex = currentView.rootView.index as S? else {
                return nil
            }

            let previousIndex = currentIndex.backward()

            return UIHostingController(rootView: IdentifiableContent(index: previousIndex, content: { parent.content(previousIndex) }))
        }

        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let currentView = viewController as? UIHostingController<Identifiable

1 / 1

continue from func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {

ChatGPTChatGPT

1 / 1

Apologies for the truncation. Here's the continuation of the pageViewController(_:viewControllerAfter:) function and the remaining code for the PagedInfiniteScrollView:

swift

    ```swift

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let currentView = viewController as? UIHostingController<IdentifiableContent<Content, S>>, let currentIndex = currentView.rootView.index as S? else { return nil }

        let nextIndex = currentIndex.forward()

        return UIHostingController(rootView: IdentifiableContent(index: nextIndex, content: { parent.content(nextIndex) }))
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed,
           let currentView = pageViewController.viewControllers?.first as? UIHostingController<IdentifiableContent<Content, S>>,
           let currentIndex = currentView.rootView.index as S? {
            parent.currentPage = currentIndex
        }
    }
}

}


Next, you need to modify the `IdentifiableContent` struct to accept the `Steppable` generic for the index:

swift

```swift
struct IdentifiableContent<Content: View, S: Steppable>: View {
    let index: S
    let content: Content

    init(index: S, @ViewBuilder content: () -> Content) {
        self.index = index
        self.content = content()
    }

    var body: some View {
        content
    }
}

Now, you can use the modified PagedInfiniteScrollView with Int or Date indices. Here's an example using Int:

swift

struct ContentView: View {
    @State private var currentPage: Int = 0

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { index in
                Text("\(index)")
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .edgesIgnoringSafeArea(.all)
            }, currentPage: $currentPage)
        }
    }
}

And here's an example using Date:

swift

struct ContentView: View {
    @State private var currentDate: Date = Date()

    var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { date in
                Text(dateFormatter.string(from: date))
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .edgesIgnoringSafeArea(.all)
            }, currentPage: $currentDate)
        }
    }
}

In both examples, the PagedInfiniteScrollView accepts the Steppable generic for the index, and the content argument has been updated to (Steppable) -> Content.

2 / 2

Steppable needs to have an origin static property. The origin of Int can be 0 and the origin of date can be Date.now

ChatGPTChatGPT

1 / 1

You can add an origin static property to the Steppable protocol, and provide default implementations for Int and Date. Here's how to do it:

  1. Add the origin static property to the Steppable protocol:

swift

protocol Steppable {
    static var origin: Self { get }
    func forward() -> Self
    func backward() -> Self
}
  1. Provide default implementations for Int and Date:

swift

extension Int: Steppable {
    static var origin: Int {
        return 0
    }

    func forward() -> Int {
        return self + 1
    }

    func backward() -> Int {
        return self - 1
    }
}

extension Date: Steppable {
    static var origin: Date {
        return Date()
    }

    func forward() -> Date {
        return Calendar.current.date(byAdding: .day, value: 1, to: self) ?? self
    }

    func backward() -> Date {
        return Calendar.current.date(byAdding: .day, value: -1, to: self) ?? self
    }
}

Now, you have an origin static property for the Steppable protocol, with default implementations for Int and Date. You can use these in your SwiftUI views as needed. For example:

swift

struct ContentView: View {
    @State private var currentPage: Int = Int.origin

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { index in
                Text("\(index)")
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .edgesIgnoringSafeArea(.all)
            }, currentPage: $currentPage)
        }
    }
}

Or, using Date:

swift

struct ContentView: View {
    @State private var currentDate: Date = Date.origin

    var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateStyle = .long
        return formatter
    }

    var body: some View {
        VStack {
            PagedInfiniteScrollView(content: { date in
                Text(dateFormatter.string(from: date))
                    .font(.largeTitle)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color.blue)
                    .edgesIgnoringSafeArea(.all)
            }, currentPage: $currentDate)
        }
    }
}

In both examples, the origin static property is used as the initial value for the currentPage or currentDate state properties.

@beader
Copy link
Author

beader commented May 2, 2023

Simulator.Screen.Recording.-.iPhone.14.-.2023-05-02.at.11.55.30.mp4

@AmirCa84
Copy link

Thank you! I liked using the Steppable protocol. It was useful for me!

@beader
Copy link
Author

beader commented Oct 22, 2023

Thank you! I liked using the Steppable protocol. It was useful for me!

Really happy to hear that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment