Created
October 31, 2022 07:34
-
-
Save dmikots/fd90a10d5f04d4c8f3df14faca6462ac to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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