How to Implement Pageview in Swiftui

How can I implement PageView in SwiftUI?

iOS 15+

In iOS 15 a new TabViewStyle was introduced: CarouselTabViewStyle (watchOS only).

Also, we can now set styles more easily:

.tabViewStyle(.page)

iOS 14+

There is now a native equivalent of UIPageViewController in SwiftUI 2 / iOS 14.

To create a paged view, add the .tabViewStyle modifier to TabView and pass PageTabViewStyle.

@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
TabView {
FirstView()
SecondView()
ThirdView()
}
.tabViewStyle(PageTabViewStyle())
}
}
}

You can also control how the paging dots are displayed:

// hide paging dots
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

You can find a more detailed explanation in this link:

  • How to create scrolling pages of content using tabViewStyle()


Vertical variant

TabView {
Group {
FirstView()
SecondView()
ThirdView()
}
.rotationEffect(Angle(degrees: -90))
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.rotationEffect(Angle(degrees: 90))


Custom component

If you're tired of passing tabViewStyle every time you can create your own PageView:

Note: TabView selection in iOS 14.0 worked differently and that's why I used two Binding properties: selectionInternal and selectionExternal. As of iOS 14.3 it seems to be working with just one Binding. However, you can still access the original code from the revision history.

struct PageView: View where SelectionValue: Hashable, Content: View {
@Binding private var selection: SelectionValue
private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
private let indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode
private let content: () -> Content

init(
selection: Binding,
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
@ViewBuilder content: @escaping () -> Content
) {
self._selection = selection
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}

var body: some View {
TabView(selection: $selection) {
content()
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: indexBackgroundDisplayMode))
}
}

extension PageView where SelectionValue == Int {
init(
indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
indexBackgroundDisplayMode: PageIndexViewStyle.BackgroundDisplayMode = .automatic,
@ViewBuilder content: @escaping () -> Content
) {
self._selection = .constant(0)
self.indexDisplayMode = indexDisplayMode
self.indexBackgroundDisplayMode = indexBackgroundDisplayMode
self.content = content
}
}

Now you have a default PageView:

PageView {
FirstView()
SecondView()
ThirdView()
}

which can be customised:

PageView(indexDisplayMode: .always, indexBackgroundDisplayMode: .always) { ... }

or provided with a selection:

struct ContentView: View {
@State var selection = 1

var body: some View {
VStack {
Text("Selection: \(selection)")
PageView(selection: $selection, indexBackgroundDisplayMode: .always) {
ForEach(0 ..< 3, id: \.self) {
Text("Page \($0)")
.tag($0)
}
}
}
}
}

SwiftUI + UIPageViewController, multiple view types

You can wrap your pages in AnyView:

PageView(pages: [
AnyView(Color.red),
AnyView(Color.blue),
AnyView(Text("Hi"))
])

SwiftUI - PageView - Pass in different Views

Try using type erasure by casting every view to AnyView:

var pageViewViewArray: [AnyView] = [AnyView(TestView()), AnyView(AnotherView()), AnyView(DifferentView())]

Documentation here, and an example of using it here.

How do I add a sliding header to my Page View in Swift UI

You can put your own buttons above the TabView, that change the selected page:

struct ContentView: View {

@State private var selectedTab = 1

var body: some View {
VStack(spacing: 0) {

HStack {
tabButton(title: "One", tag: 1)
tabButton(title: "Two", tag: 2)
tabButton(title: "Three", tag: 3)
}
.padding(.top)
.font(.headline)

TabView(selection: $selectedTab) {
Text("First Tab").tag(1)
Text("Second Tab").tag(2)
Text("Third Tab").tag(3)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.background(.gray.opacity(0.5))
}
}

func tabButton(title: String, tag: Int) -> some View {
VStack {
Button(title) { withAnimation { selectedTab = tag } }
.frame(maxWidth: .infinity)
.foregroundColor(selectedTab == tag ? .primary : .secondary)

Color(selectedTab == tag ? .blue : .clear)
.frame(height: 4)
.padding(.horizontal)
}
}
}

Sample Image

SwiftUI send action from a page to the PageViewController

OK, I figured it out. Not intuitive at first, I have to say. Coming from traditional event based programming, it's quite a different way of thinking

I used a @State variable in the main instance of the view.

I used @Binding variables to deal with the state both upstream (ViewControllers, Controls) and downstream (subviews). So, for example, I used a variable to tell the dataSource of the UIPageViewController if or not to return a view controller before/after the current one.

For the dismissing the modally presented controller I used

@Environment(\.presentationMode) var presentationMode

...

func dismiss() {
self.presentationMode.wrapptedValue.dismiss()
}

Similarly,

...
@Binding var currentPage: int
...

Button(action: next) { Text("Next Page") }
...

...
func next() {
currentPage += 1
}

There were a few caveats in deciding how to nest the views and what variables to pick for the bindings, but it is clear to me now. The biggest problem was ultimately where the "source of truth" should be anchored. It turned out, right in the "middle", i.e. below the controller and above the particular views.

Hope this is useful for others looking for something similar.



Related Topics



Leave a reply



Submit