PageViewer
UIPageViewController adapted for use in SwiftUi AnyView
Version 2 changes
- Reactive modifiers.
- New display and transition logic
- Delegate to receive events
- External controller for software control of transitions
- You can independently determine which safe areas to ignore and which not
- Full control over the appearance of indicators
- Gesture lock
- AnyView is no longer used when creating an array of views for a UIPageViewController
- Added documentation with examples
- Fixed crash of simultaneous navigation with gesture when changing index via anchor
- Fixed loss of Binding on multiple new values at times completed
- Limited transitions when multiple assigning a new value to the Binding index (if you quickly tap on the button that changes the index), which could lead to crashes
Installation
Swift Package Manager
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift
compiler.
Once you have your Swift package set up, adding PageViewer as a dependency is as easy as adding it to the dependencies
value of your Package.swift
.
dependencies: [
.package(url: "https://github.com/cbepxbeo/page-viewer.git", .upToNextMajor(from: "0.0.1"))
]
Usage
If you don’t want to, then there is no need to create your own providers. the package provides everything you need by default.
Generating a Vew with Binding from a collection using an index and an element.
@State var index: Int = 0
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection, index: $index) { index, element in
VStack {
Text("Element: \(element)")
Text("Index: \(index)")
}
}
}
Generating a Vew without Binding from a collection using an index and an element.
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection) { index, element in
VStack {
Text("Element: \(element)")
Text("Index: \(index)")
}
}
}
Generating a Vew with Binding from a collection using an element.
@State var index: Int = 0
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection, index: $index) { element in
VStack {
Text("Element: \(element)")
}
}
}
Generating a Vew without Binding from a collection using an element.
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection) { element in
VStack {
Text("Element: \(element)")
}
}
}
Create from ready-made representations in an array with Binding
@State var index: Int = 0
let array: [Color] = [
.red,
.blue,
.green
]
var body: some View {
PageView(views: array, index: $index)
}
Create from ready-made representations in an array without Binding
let array: [Color] = [
.red,
.blue,
.green
]
var body: some View {
PageView(views: array)
}
If you are using full screen output, then you need to specify it for the required views.
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection) { index, element in
if index == 0 {
ZStack {
Color.red
Text("Element: \(element)")
}
.ignoresSafeArea() //Will ignore
} else {
ZStack {
Color.green
Text("Element: \(element)")
} //Will not
}
}
}
You can take full advantage of the ViewBuilder and create unique views for each element
@State var index: Int = 0
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection, index: $index) { element in
if element == 1 {
ZStack{
Color.red
Text("First")
}
} else {
Text("Other element. Element: \(element)")
}
}
}
Set and create delegate
final class ExampleViewModel: ObservableObject, PageViewDelegate {
//Required by a delegate
func willTransition(_ pendingIndex: Int) {
//Your code
}
//Required by a delegate
func didFinishAnimating(_ completed: Bool) {
//Your code
}
//Required by a delegate
func indexAfterAnimation(_ index: Int) {
//Your code
}
}
struct ExampleView: View {
@StateObject var viewModel = ExampleViewModel()
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection) { element in
Text("Element: \(element)")
}
.delegate(self.viewModel)
}
}
Use a delegate to access the actual index.
final class ExampleViewModel: ObservableObject, PageViewDelegate {
//Required by a delegate
func willTransition(_ pendingIndex: Int) {
//Your code
}
//Required by a delegate
func didFinishAnimating(_ completed: Bool) {
//Your code
}
//Property to store index and view notifications
@Published
var currentIndex: Int = 0
//Delegate method that receives the index after the transition animation.
//Runs on the main thread
func indexAfterAnimation(_ index: Int) {
self.currentIndex = index
}
}
struct ExampleView: View {
@StateObject var viewModel = ExampleViewModel()
@State var index: Int = 0 //Do not use for state control in modifiers
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection, index: $index) { index, element in
ZStack {
Color.red
if index != 1 {
Text("Element: \(element)")
} else {
//Appears in a view with scroll disabled and allows
//you to navigate to the next view
Button("Go to view at index 2"){
self.index = 2 //You can assign for switching
}
}
}
}
.delegate(self.viewModel) //Installing a delegate
//Disable scroll gestures for view at index 1
.scrollEnabled(self.viewModel.currentIndex != 1) //Correct
//.scrollEnabled(self.index != 1) Wrong
}
}
Set and create external controller
final class ExampleViewModel: ObservableObject, PageViewController {
//Required by a controller
var pageViewCoordinator: CoordinatorStorage?
}
struct ExampleView: View {
@StateObject var viewModel = ExampleViewModel()
let collection: [Int] = [1,2,3,4,5]
var body: some View {
VStack{
PageView(collection, index: $index) { element in
Text("Element: \(element)")
}
.controller(self.viewModel)
Button("Go to next"){
self.viewModel.go(to: .next)
}
//The go method is provided by default
}
}
}
Setting indicators
struct ExampleIndicatorsStyle: IndicatorStyle {
func makeIndicator(selected: Bool, index: Int) -> some View {
Circle()
.fill(selected ? .red : .green)
.frame(width: 30, height: 30)
}
func makeConfiguredPageView(
content: () -> Content,
indicators: () -> Indicators) -> some View {
VStack{
content()
HStack{
indicators()
}
.padding(.top, 50)
}
}
}
struct ExampleView: View {
let collection: [Int] = [1,2,3,4,5]
var body: some View {
PageView(collection) { element in
Text("Element: \(element)")
}
.indicators(ExampleIndicatorsStyle())
}
}
Note
More information is documented in the package.