Skip to content

Instantly share code, notes, and snippets.

@dmikots
Created October 31, 2022 07:34
Show Gist options
  • Save dmikots/fd90a10d5f04d4c8f3df14faca6462ac to your computer and use it in GitHub Desktop.
Save dmikots/fd90a10d5f04d4c8f3df14faca6462ac to your computer and use it in GitHub Desktop.
import Foundation
import SwiftUI
// MARK: - PageViewerHostingController
final internal class PageViewerHostingController<Content>: UIHostingController<Content>
where Content: View {
// MARK: Lifecycle
internal init(index: Int, rootView: Content) {
self.index = index
super.init(rootView: rootView)
}
@MainActor
required dynamic internal init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Internal
private(set) var index: Int
}
// MARK: - PageViewerUIWrapper
internal struct PageViewerUIWrapper<T>: UIViewControllerRepresentable where T: View {
// MARK: Lifecycle
internal init(
_ forceMoveToNextPoint: Bool,
_ views: [T],
_ currentIndex: Binding<Int>?,
_ currentPage: Binding<Int>?,
_ pointsPage: Binding<Int>
) {
self.views = views
self.currentIndex = currentIndex
self.currentPage = currentPage
self.pointsPage = pointsPage
self.forceMoveToNextPoint = forceMoveToNextPoint
}
// MARK: Internal
internal let pointsPage: Binding<Int>
internal func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal
)
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
if let root = context.coordinator.root {
pageViewController.setViewControllers(
[root], direction: .forward, animated: true
)
}
return pageViewController
}
internal func makeCoordinator() -> PagesViewerCoordinator<T> {
PagesViewerCoordinator(forceMoveToNextPoint, views, currentIndex, currentPage, pointsPage)
}
internal func updateUIViewController(
_ pageViewController: UIPageViewController,
context: Context
) {
let last: Int,
count: Int,
direction: UIPageViewController.NavigationDirection
var index: Int
count = context.coordinator.controllers.count
last = context.coordinator.lastIndex
if let currentIndex = currentIndex?.wrappedValue {
index = currentIndex
} else if let currentPage = currentPage?.wrappedValue {
index = currentPage - 1
} else {
return
}
if index >= count, last < count {
print("Индекс или Номер страницы вышли за допустимые пределы")
DispatchQueue.main.async {
self.currentPage?.wrappedValue = 1
self.currentIndex?.wrappedValue = 0
}
index = 0
}
direction = index > last ? .forward : .reverse
if last == index { return }
DispatchQueue.main.async {
context.coordinator.lastIndex = index
pageViewController.setViewControllers(
[context.coordinator.controllers[index]], direction: direction, animated: true
)
if context.coordinator.pointsPage.wrappedValue != index {
context.coordinator.pointsPage.wrappedValue = index
}
}
}
// MARK: Private
private let views: [T]
private let currentIndex: Binding<Int>?
private let currentPage: Binding<Int>?
private let forceMoveToNextPoint: Bool
}
// MARK: - PageViewerView
public struct PageViewerView<A: RandomAccessCollection, C: View>: View {
// MARK: Lifecycle
// ----------public
public init(
_ array: A,
currentIndex: Binding<Int>,
@ViewBuilder content: @escaping (A.Index, A.Element) -> C
) {
self.init(array, currentIndex: currentIndex, currentPage: nil, content: content)
}
public init(
_ array: A,
currentPage: Binding<Int>,
@ViewBuilder content: @escaping (A.Index, A.Element) -> C
) {
self.init(array, currentIndex: nil, currentPage: currentPage, content: content)
}
public init(
_ array: A,
currentIndex: Binding<Int>,
@ViewBuilder content: @escaping (A.Element) -> C
) {
self.init(array, currentIndex: currentIndex, currentPage: nil, content: content)
}
public init(
_ array: A,
currentPage: Binding<Int>,
@ViewBuilder content: @escaping (A.Element) -> C
) {
self.init(array, currentIndex: nil, currentPage: currentPage, content: content)
}
public init(_ array: A, @ViewBuilder content: @escaping (A.Element) -> C) {
self.init(array, currentIndex: nil, currentPage: nil, content: content)
}
public init(_ array: A, @ViewBuilder content: @escaping (A.Index, A.Element) -> C) {
self.init(array, currentIndex: nil, currentPage: nil, content: content)
}
private init(
_ array: A,
currentIndex: Binding<Int>? = nil,
currentPage: Binding<Int>? = nil,
@ViewBuilder content: @escaping (A.Index, A.Element) -> C
) {
self.views = Array(zip(array.indices, array)).map { index, element in
content(index, element)
}
self.currentIndex = currentIndex
self.currentPage = currentPage
}
private init(
_ array: A,
currentIndex: Binding<Int>? = nil,
currentPage: Binding<Int>? = nil,
@ViewBuilder content: @escaping (A.Element) -> C
) {
self.views = Array(array).map { content($0) }
self.currentIndex = currentIndex
self.currentPage = currentPage
}
// MARK: Public
// MARK: view
public var body: some View {
PageViewerUIWrapper(
forceMoveToNextPoint,
views,
currentIndex,
currentPage,
$indexToPoint
)
}
// MARK: mod.
public func pagePoints(_ showPoints: Bool) -> PageViewerView {
var view = self
view.showPoints = showPoints
return view
}
public func forceMove(_ forceMoveToNextPoint: Bool) -> PageViewerView {
var view = self
view.forceMoveToNextPoint = forceMoveToNextPoint
return view
}
// MARK: Private
// -----default
@State
private var indexToPoint: Int = 0
private var showPoints: Bool = false
private var forceMoveToNextPoint: Bool = true
// -----from init
private let currentIndex: Binding<Int>?
private let currentPage: Binding<Int>?
private let views: [C]
}
extension PageViewerView where A == [Any] {
public init(views: [C], currentIndex: Binding<Int>) {
self.views = views
self.currentIndex = currentIndex
self.currentPage = nil
}
public init(views: [C], currentPage: Binding<Int>) {
self.views = views
self.currentIndex = nil
self.currentPage = currentPage
}
public init(views: [C]) {
self.views = views
self.currentIndex = nil
self.currentPage = nil
}
}
// MARK: - PagesViewerCoordinator
final internal class PagesViewerCoordinator<T>: NSObject, UIPageViewControllerDataSource,
UIPageViewControllerDelegate where T: View {
// MARK: Lifecycle
internal init(
_ forceMoveToNextPoint: Bool,
_ views: [T],
_ currentIndex: Binding<Int>?,
_ currentPage: Binding<Int>?,
_ pointsPage: Binding<Int>
) {
var temp: [Hosting<AnyView>] = []
for (index, element) in views.enumerated() {
temp.append(Hosting(index: index, rootView: AnyView(element.ignoresSafeArea())))
}
self.pointsPage = pointsPage
self.forceMoveToNextPoint = forceMoveToNextPoint
self.controllers = temp
self.currentIndex = currentIndex
self.currentPage = currentPage
self.root = controllers.first
self.lastIndex = currentIndex?.wrappedValue ?? ((currentPage?.wrappedValue ?? 1) - 1)
}
// MARK: Internal
typealias Hosting = PageViewerHostingController
internal let controllers: [Hosting<AnyView>]
internal let root: Hosting<AnyView>?
internal let pointsPage: Binding<Int>
internal var lastIndex: Int
internal func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
guard let hosting = viewController as? Hosting<AnyView>
else {
return nil
}
let index = hosting.index == 0 ? controllers.count - 1 : hosting.index - 1
lastIndex = index
return controllers[index]
}
internal func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
guard let hosting = viewController as? Hosting<AnyView>
else {
return nil
}
let index = hosting.index + 1 == controllers.count ? 0 : hosting.index + 1
lastIndex = index
return controllers[index]
}
internal func pageViewController(
_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool
) {
guard let hosting = pageViewController.viewControllers?.first as? Hosting<AnyView>
else {
return
}
DispatchQueue.main.async {
self.currentIndex?.wrappedValue = hosting.index
self.currentPage?.wrappedValue = hosting.index + 1
if hosting.index != self.pointsPage.wrappedValue {
self.pointsPage.wrappedValue = hosting.index
}
}
}
internal func pageViewController(
_ pageViewController: UIPageViewController,
willTransitionTo pendingViewControllers: [UIViewController]
) {
guard let hosting = pendingViewControllers.first as? Hosting<AnyView>,
forceMoveToNextPoint
else {
return
}
DispatchQueue.main.async {
if hosting.index != self.pointsPage.wrappedValue {
self.pointsPage.wrappedValue = hosting.index
}
}
}
// MARK: Private
private let currentIndex: Binding<Int>?
private let currentPage: Binding<Int>?
private let forceMoveToNextPoint: Bool
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment