Skip to content

Instantly share code, notes, and snippets.

@andreyz
Created February 28, 2023 15:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andreyz/d876fcacd993250e49c545dd793120d8 to your computer and use it in GitHub Desktop.
Save andreyz/d876fcacd993250e49c545dd793120d8 to your computer and use it in GitHub Desktop.
An example of how to create a lazy loaded paged view in SwiftUI
import SwiftUI
/**
A container view that manages navigation between pages of content.
*/
public struct PageView<Content: View, Item: Hashable> {
public typealias ItemProvider = (Item) -> Item?
public typealias ViewProvider = (Item) -> Content
/// The style for transitions between pages.
public enum TransitionStyle {
case scroll
case pageCurl
}
@Binding var selection: Item
let style: TransitionStyle
let axis: Axis
let spacing: Int
let prev: ItemProvider
let next: ItemProvider
@ViewBuilder let content: (Item) -> Content
public init(selection: Binding<Item>, style: TransitionStyle = .scroll, axis: Axis = .horizontal, spacing: Int = 10, prev: @escaping ItemProvider, next: @escaping ItemProvider, @ViewBuilder content: @escaping ViewProvider) {
_selection = selection
self.style = style
self.axis = axis
self.spacing = spacing
self.prev = prev
self.next = next
self.content = content
}
}
extension PageView: UIViewControllerRepresentable {
public typealias UIViewControllerType = UIPageViewController
public func makeUIViewController(context: Context) -> UIPageViewController {
let viewController = UIPageViewController(
transitionStyle: style.uiPageViewController,
navigationOrientation: axis.uiPageViewController,
options: [.interPageSpacing: spacing]
)
viewController.delegate = context.coordinator
viewController.dataSource = context.coordinator
let initialView = ItemHostingController(item: selection, view: content(selection))
viewController.setViewControllers([initialView], direction: .forward, animated: false)
return viewController
}
public func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
let isAnimated = context.transaction.animation != nil
goTo(selection, pageViewController: uiViewController, animated: isAnimated)
}
// MARK: - Navigation
func goTo(_ item: Item, pageViewController: UIPageViewController, animated: Bool = true) {
guard let currentViewController = pageViewController.viewControllers?.first as? ItemHostingController<Item> else {
return
}
guard currentViewController.item != item else {
return
}
let viewController = ItemHostingController(item: item, view: content(item))
pageViewController.setViewControllers([viewController], direction: .forward, animated: animated)
}
// MARK: - Coordinator
public func makeCoordinator() -> Coordinator {
Coordinator(self)
}
public class Coordinator: NSObject, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
let pageView: PageView
init(_ pageView: PageView) {
self.pageView = pageView
}
// MARK: - Data Source
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? ItemHostingController<Item> else {
return nil
}
if let prev = pageView.prev(viewController.item) {
return makeView(prev)
} else {
return nil
}
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewController = viewController as? ItemHostingController<Item> else {
return nil
}
if let next = pageView.next(viewController.item) {
print("Requesting item after \(viewController.item)")
return makeView(next)
} else {
print("Nothing after \(viewController.item)")
return nil
}
}
// MARK: - Delegate
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard let viewController = pageViewController.viewControllers?.first as? ItemHostingController<Item> else {
return
}
let item = viewController.item
if pageView.selection != item {
pageView.selection = item
}
}
// MARK: - Helpers
func makeView(_ item: Item) -> PageView.ItemHostingController<Item> {
ItemHostingController(item: item, view: pageView.content(item))
}
}
class ItemHostingController<Item>: UIHostingController<Content> {
let item: Item
init(item: Item, view: Content) {
self.item = item
super.init(rootView: view)
self.view.backgroundColor = .clear
self.view.isOpaque = false
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
extension PageView.TransitionStyle {
var uiPageViewController: UIPageViewController.TransitionStyle {
switch self {
case .scroll:
return .scroll
case .pageCurl:
return .pageCurl
}
}
}
extension Axis {
var uiPageViewController: UIPageViewController.NavigationOrientation {
switch self {
case .horizontal:
return .horizontal
case .vertical:
return .vertical
}
}
}
struct PageView_Previews: PreviewProvider {
static var previews: some View {
CounterDemoView()
}
struct CounterDemoView: View {
// Selection can be anything hashable (in practice, probably your model ID)
@State private var selection: Int = 1
var body: some View {
VStack {
PageView(selection: $selection, prev: prev, next: next) { item in
// This closure will be called each time a new page is needed
Text("Number \(item)")
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.animation(.default, value: selection)
Text("Current Number: \(selection)")
// So we can test set setting the selection from SwiftUI
Button {
if let next = next(selection) {
selection = next
}
} label: {
Text("Next")
}
}
}
// Return the item that comes before
func prev(_ item: Int) -> Int? {
if item <= 0 {
return nil
} else {
return item - 1
}
}
// Return the item that comes after
func next(_ item: Int) -> Int? {
if item > 9 {
return nil
} else {
return item + 1
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment