Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwiftUI Wrapper(Page View) refactoring #551

Merged
merged 4 commits into from
Apr 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions ExampleSwiftUI/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@
import UIKit
import SwiftUI
import Parchment
import SwiftUI
import UIKit

struct ContentView: View {
var body: some View {
return PageView(items: [
PagingIndexItem(index: 0, title: "View 0"),
PagingIndexItem(index: 1, title: "View 1"),
PagingIndexItem(index: 2, title: "View 2"),
PagingIndexItem(index: 3, title: "View 3")
]) { item in
Text(item.title)
.font(.largeTitle)
.foregroundColor(.gray)
let items = [
PagingIndexItem(index: 0, title: "View 0"),
PagingIndexItem(index: 1, title: "View 1"),
PagingIndexItem(index: 2, title: "View 2"),
PagingIndexItem(index: 3, title: "View 3"),
PagingIndexItem(index: 4, title: "View 4"),
]
@State
var scrollToPosition: PageViewScrollPosition?

var body: some View {
VStack {
Button(action: {
scrollToPosition = PageViewScrollPosition(index: (0 ... 3).randomElement()!)
}) {
Text("Random Index")
.font(.largeTitle)
}
PageView(scrollToPosition: $scrollToPosition, items: items) { _ in
List(0 ..< 100) { index in
Text(String(index))
.font(.largeTitle)
.foregroundColor(.gray)
}
}
.willScroll { pagingItem in
print("willScroll: \(pagingItem)")
}
.didScroll { pagingItem in
print("didScroll: \(pagingItem)")
}
.didSelect { pagingItem in
print("didSelect: \(pagingItem)")
}
}
}
}

extension ContentView: Equatable {
static func == (lhs: ContentView, rhs: ContentView) -> Bool {
lhs.items == rhs.items
}
}
}
2 changes: 1 addition & 1 deletion ExampleSwiftUI/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
let contentView = ContentView().equatable()

if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
Expand Down
234 changes: 160 additions & 74 deletions Parchment/Structs/PageView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import UIKit
import SwiftUI
import UIKit

/// Check if both SwiftUI and Combine is available. Without this
/// xcodebuild fails, saying it can't find the SwiftUI types used
Expand All @@ -9,81 +9,167 @@ import SwiftUI
/// https://forums.swift.org/t/weak-linking-of-frameworks-with-greater-deployment-targets/26017/24
#if canImport(SwiftUI) && canImport(Combine)

/// `PageView` provides a SwiftUI wrapper around `PagingViewController`.
/// It can be used with any fixed array of `PagingItem`s. Use the
/// `PagingOptions` struct to customize the properties.
@available(iOS 13.0, *)
public struct PageView<Item: PagingItem, Page: View>: View {
private let items: [Item]
private let options: PagingOptions
private let content: (Item) -> Page

/// Initialize a new `PageView`.
///
/// - Parameters:
/// - options: The configuration parameters we want to customize.
/// - items: The array of `PagingItem`s to display in the menu.
/// - content: A callback that returns the `View` for each item.
public init(
options: PagingOptions = PagingOptions(),
items: [Item],
content: @escaping (Item) -> Page) {
self.options = options
self.items = items
self.content = content
}

public var body: some View {
PagingController(
items: items,
options: options,
content: content)
}

struct PagingController: UIViewControllerRepresentable {
let items: [Item]
let options: PagingOptions
let content: (Item) -> Page

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

func makeUIViewController(context: UIViewControllerRepresentableContext<PagingController>) -> PagingViewController {
let pagingViewController = PagingViewController(options: options)
return pagingViewController
}

func updateUIViewController(_ pagingViewController: PagingViewController, context: UIViewControllerRepresentableContext<PagingController>) {
context.coordinator.parent = self

if pagingViewController.dataSource == nil {
pagingViewController.dataSource = context.coordinator
} else {
pagingViewController.reloadData()
}
}
}
/// `PageView` provides a SwiftUI wrapper around `PagingViewController`.
/// It can be used with any fixed array of `PagingItem`s. Use the
/// `PagingOptions` struct to customize the properties.
@available(iOS 13.0, *)
public struct PageView<Item: PagingItem, Page: View>: View where Item: Hashable {
public typealias WillScrollCallback = ((PagingItem) -> Void)
public typealias DidScrollCallback = ((PagingItem) -> Void)
public typealias DidSelectCallback = ((PagingItem) -> Void)
private let options: PagingOptions
private var items = [Item]()
let content: (Item) -> Page
@Binding
private var scrollToPosition: PageViewScrollPosition?
var willScrollCallback: WillScrollCallback?
var didScrollCallback: DidScrollCallback?
var didSelectCallback: DidSelectCallback?

class Coordinator: PagingViewControllerDataSource {
var parent: PagingController

init(_ pagingController: PagingController) {
self.parent = pagingController
}

func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int {
return parent.items.count
/// Initialize a new `PageView`.
///
/// - Parameters:
/// - options: The configuration parameters we want to customize.
/// - items: The array of `PagingItem`s to display in the menu.
/// - content: A callback that returns the `View` for each item.
public init(options: PagingOptions = PagingOptions(),
scrollToPosition: Binding<PageViewScrollPosition?>? = nil,
items: [Item],
content: @escaping (Item) -> Page)
{
self._scrollToPosition = scrollToPosition ?? .constant(nil)
self.options = options
self.items = items
self.content = content
}

public var body: some View {
PagingController(items: items,
options: options,
content: content,
scrollToPosition: $scrollToPosition,
willScrollCallback: willScrollCallback,
didScrollCallback: didScrollCallback,
didSelectCallback: didSelectCallback)
}

struct PagingController: UIViewControllerRepresentable {
let items: [Item]
let options: PagingOptions
let content: (Item) -> Page
var viewControllers = [Item: UIHostingController<Page>]()
@Binding
var scrollToPosition: PageViewScrollPosition?
var willScrollCallback: WillScrollCallback?
var didScrollCallback: DidScrollCallback?
var didSelectCallback: DidSelectCallback?

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

func makeUIViewController(context: UIViewControllerRepresentableContext<PagingController>) -> PagingViewController {
let pagingViewController = PagingViewController(options: options)
pagingViewController.dataSource = context.coordinator
pagingViewController.delegate = context.coordinator
return pagingViewController
}

func updateUIViewController(_ pagingViewController: PagingViewController,
context: UIViewControllerRepresentableContext<PagingController>)
{
context.coordinator.parent = self

if let position = $scrollToPosition.wrappedValue {
pagingViewController.select(index: position.index, animated: position.animated)
} else {
pagingViewController.reloadData()
}
}
}

class Coordinator: NSObject, PagingViewControllerDataSource, PagingViewControllerDelegate {
var parent: PagingController

init(_ pagingController: PagingController) {
self.parent = pagingController
}

func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int {
parent.items.count
}

func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
guard let viewController = parent.viewControllers[parent.items[index]] else {
let view = parent.content(parent.items[index])
let viewController = UIHostingController(rootView: view)
parent.viewControllers[parent.items[index]] = viewController
return viewController
}
return viewController
}

func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
parent.items[index]
}

func pagingViewController(_ pagingViewController: PagingViewController,
didScrollToItem pagingItem: PagingItem,
startingViewController: UIViewController?,
destinationViewController: UIViewController,
transitionSuccessful: Bool)
{
parent.didScrollCallback?(pagingItem)

DispatchQueue.main.async {
self.parent.scrollToPosition = nil
}
}

func pagingViewController(_ pagingViewController: PagingViewController,
willScrollToItem pagingItem: PagingItem,
startingViewController: UIViewController,
destinationViewController: UIViewController)
{
parent.willScrollCallback?(pagingItem)
}

func pagingViewController(_ pagingViewController: PagingViewController, didSelectItem pagingItem: PagingItem) {
parent.didSelectCallback?(pagingItem)
}
}
}

func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController {
let view = parent.content(parent.items[index])
return UIHostingController(rootView: view)

@available(iOS 13.0, *)
public struct PageViewScrollPosition: Equatable {
public var index: Int
public var animated: Bool

public init(index: Int, animated: Bool = true) {
self.index = index
self.animated = animated
}
}

func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem {
return parent.items[index]

@available(iOS 13.0, *)
public extension PageView {
func didScroll(_ didScrollCallback: @escaping DidScrollCallback) -> Self {
var this = self
this.didScrollCallback = didScrollCallback
return this
}

func willScroll(_ willScrollCallback: @escaping WillScrollCallback) -> Self {
var this = self
this.willScrollCallback = willScrollCallback
return this
}

func didSelect(_ didSelectCallback: @escaping DidSelectCallback) -> Self {
var this = self
this.didSelectCallback = didSelectCallback
return this
}
}
}
}

#endif