How can I implement PageView?
Asked Answered
S

7

51

I am new to SwiftUI. I have three views and I want them in a PageView. I want to move each Views by swipe like a pageview and I want the little dots to indicate in which view I'm in.

Shire answered 15/10, 2019 at 5:57 Comment(4)
Try using UICollectionView. Here's a great tutorial: youtube.com/watch?v=a5yjOMLBfScLebaron
SwiftUIX has a SwiftUI wrapper for UIPageViewController - see PaginatedViewsContent.swift.Agrigento
Please check out this . It is pure SwiftUI, so I found the lifecycle easier to manage. Also, you can write any custom SwiftUI code in that.Ephesus
For the pager, check out out thisEphesus
R
112

iOS 15+

In iOS 15 we can now set a page style in an easy way:

TabView {
    FirstView()
    SecondView()
    ThirdView()
}
.tabViewStyle(.page)

We can also set the visibility of indices:

.tabViewStyle(.page(indexDisplayMode: .always))

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:


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<SelectionValue, Content>: 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<SelectionValue>,
        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)
                }
            }
        }
    }
}
Racon answered 29/7, 2020 at 18:9 Comment(4)
Hey @pawello2222, awesome code! Implements nicely with swifty syntax. As of right now, however, the indicator dots do not update with the selected view when not using a ForEach loop. If I manually type out: MyView(text: message[0]) ... MyView(text: message[n]); regardless of which view I swipe to, the indicator remains at index[0].Vyner
@JustinBush Unfortunately, the SwiftUI internal implementation can change with every iOS release. TabView selection no longer works in the same way as in iOS 14.0, so I updated my answer with a new implementation.Racon
Your PageView works - but unfortunately adds up an awful lot of memory when swiping complex Views. Any idea why ? (I'm on iOS 14.4.2)Otey
There is a big disadvantage in that implementation - it's not lazy like UIpageviewcontrollerQuincey
F
34

Page Control

struct PageControl: UIViewRepresentable {
    var numberOfPages: Int
    @Binding var currentPage: Int
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIPageControl {
        let control = UIPageControl()
        control.numberOfPages = numberOfPages
        control.pageIndicatorTintColor = UIColor.lightGray
        control.currentPageIndicatorTintColor = UIColor.darkGray
        control.addTarget(
            context.coordinator,
            action: #selector(Coordinator.updateCurrentPage(sender:)),
            for: .valueChanged)

        return control
    }

    func updateUIView(_ uiView: UIPageControl, context: Context) {
        uiView.currentPage = currentPage
    }

    class Coordinator: NSObject {
        var control: PageControl

        init(_ control: PageControl) {
            self.control = control
        }
        @objc
        func updateCurrentPage(sender: UIPageControl) {
            control.currentPage = sender.currentPage
        }
    }
}

Your page View

struct PageView<Page: View>: View {
    var viewControllers: [UIHostingController<Page>]
    @State var currentPage = 0
    init(_ views: [Page]) {
        self.viewControllers = views.map { UIHostingController(rootView: $0) }
    }

    var body: some View {
        ZStack(alignment: .bottom) {
            PageViewController(controllers: viewControllers, currentPage: $currentPage)
            PageControl(numberOfPages: viewControllers.count, currentPage: $currentPage)
        }
    }
}

Your page View Controller


struct PageViewController: UIViewControllerRepresentable {
    var controllers: [UIViewController]
    @Binding var currentPage: Int
    @State private var previousPage = 0

    init(controllers: [UIViewController],
         currentPage: Binding<Int>)
    {
        self.controllers = controllers
        self._currentPage = currentPage
        self.previousPage = currentPage.wrappedValue
    }

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

    func makeUIViewController(context: Context) -> UIPageViewController {
        let pageViewController = UIPageViewController(
            transitionStyle: .scroll,
            navigationOrientation: .horizontal)
        pageViewController.dataSource = context.coordinator
        pageViewController.delegate = context.coordinator

        return pageViewController
    }

    func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
        guard !controllers.isEmpty else {
            return
        }
        let direction: UIPageViewController.NavigationDirection = previousPage < currentPage ? .forward : .reverse
        context.coordinator.parent = self
        pageViewController.setViewControllers(
            [controllers[currentPage]], direction: direction, animated: true) { _ in {
            previousPage = currentPage
        }
    }

    class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
        var parent: PageViewController

        init(_ pageViewController: PageViewController) {
            self.parent = pageViewController
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerBefore viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index == 0 {
                return parent.controllers.last
            }
            return parent.controllers[index - 1]
        }

        func pageViewController(
            _ pageViewController: UIPageViewController,
            viewControllerAfter viewController: UIViewController) -> UIViewController? {
            guard let index = parent.controllers.firstIndex(of: viewController) else {
                return nil
            }
            if index + 1 == parent.controllers.count {
                return parent.controllers.first
            }
            return parent.controllers[index + 1]
        }

        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            if completed,
                let visibleViewController = pageViewController.viewControllers?.first,
                let index = parent.controllers.firstIndex(of: visibleViewController) {
                parent.currentPage = index
            }
        }
    }
}

Let's say you have a view like

struct CardView: View {
    var album: Album
    var body: some View {
        URLImage(URL(string: album.albumArtWork)!)
            .resizable()
            .aspectRatio(3 / 2, contentMode: .fit)
    }
}

You can use this component in your main SwiftUI view like this.

PageView(vM.Albums.map { CardView(album: $0) }).frame(height: 250)
Fung answered 15/10, 2019 at 13:51 Comment(4)
What does the vm.Albums part look like? Is it data that you're passing into the PageView?Comstockery
@BrodyHigby Yes. its an array of albumsFung
Awesome stuff! Seems to work great for me on iOS 16 and lets me hook into more things than SwiftUI's TabView does. Might be worth knowing you can do away with the custom PageControl if you implement UIPageViewControllerDataSource's presentationCount and presentationIndex as it'll show its own.Planoconcave
Note: There's an extra opening brace in the setViewControllers trailing closure, in updateUIViewController that breaks compilation.Planoconcave
D
4

Swift 5

To implement a page view in swiftUI, Just we need to use a TabView with a page style, I'ts really really easy. I like it

struct OnBoarding: View {
    var body: some View {
        TabView {
            Page(text:"Page 1")
            Page(text:"Page 2")
        }
        .tabViewStyle(.page(indexDisplayMode: .always))
        .ignoresSafeArea()
    }
}
Dusen answered 17/6, 2023 at 17:7 Comment(1)
You wrote about Swift 5, but I think its better to mention the OS version like macOS11+ or iOS 14+.Bum
R
3

iOS 13+ (private API)

Warning: The following answer uses private SwiftUI methods that aren't publicly visible (you can still access them if you know where to look). However, they are not documented properly and may be unstable. Use them at your own risk.

While browsing SwiftUI files I stumbled upon the _PagingView that seems to be available since iOS 13:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingView<Views> : SwiftUI.View where Views : Swift.RandomAccessCollection, Views.Element : SwiftUI.View, Views.Index : Swift.Hashable

This view has two initialisers:

public init(config: SwiftUI._PagingViewConfig = _PagingViewConfig(), page: SwiftUI.Binding<Views.Index>? = nil, views: Views)
public init(direction: SwiftUI._PagingViewConfig.Direction, page: SwiftUI.Binding<Views.Index>? = nil, views: Views)

What we also have is the _PagingViewConfig:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct _PagingViewConfig : Swift.Equatable {
  public enum Direction {
    case vertical
    case horizontal
    public static func == (a: SwiftUI._PagingViewConfig.Direction, b: SwiftUI._PagingViewConfig.Direction) -> Swift.Bool
    public var hashValue: Swift.Int {
      get
    }
    public func hash(into hasher: inout Swift.Hasher)
  }
  public var direction: SwiftUI._PagingViewConfig.Direction
  public var size: CoreGraphics.CGFloat?
  public var margin: CoreGraphics.CGFloat
  public var spacing: CoreGraphics.CGFloat
  public var constrainedDeceleration: Swift.Bool
  public init(direction: SwiftUI._PagingViewConfig.Direction = .horizontal, size: CoreGraphics.CGFloat? = nil, margin: CoreGraphics.CGFloat = 0, spacing: CoreGraphics.CGFloat = 0, constrainedDeceleration: Swift.Bool = true)
  public static func == (a: SwiftUI._PagingViewConfig, b: SwiftUI._PagingViewConfig) -> Swift.Bool
}

Now, we can create a simple _PagingView:

_PagingView(direction: .horizontal, views: [
    AnyView(Color.red),
    AnyView(Text("Hello world")),
    AnyView(Rectangle().frame(width: 100, height: 100))
])

Here is another, more customised example:

struct ContentView: View {
    @State private var selection = 1
    
    var body: some View {
        _PagingView(
            config: _PagingViewConfig(
                direction: .vertical,
                size: nil,
                margin: 10,
                spacing: 10,
                constrainedDeceleration: false
            ),
            page: $selection,
            views: [
                AnyView(Color.red),
                AnyView(Text("Hello world")),
                AnyView(Rectangle().frame(width: 100, height: 100))
            ]
        )
    }
}
Racon answered 28/2, 2021 at 16:3 Comment(2)
Am I right to assume this won't be accepted in an App Store application?Parfleche
@Parfleche TBH I don't really know - never tried it myself. I assume it won't be accepted because it uses private undocumented methods but I'm not 100% sure. This might help you: How does Apple know you are using private API?Racon
F
2

For apps that target iOS 14 and later, the answer suggested by @pawello2222 should be considered the correct one. I have tried it in two apps now and it works great, with very little code.

I have wrapped the proposed concept in a struct that can be provided with both views as well as with an item list and a view builder. It can be found here. The code looks like this:

@available(iOS 14.0, *)
public struct MultiPageView: View {
    
    public init<PageType: View>(
        pages: [PageType],
        indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
        currentPageIndex: Binding<Int>) {
        self.pages = pages.map { AnyView($0) }
        self.indexDisplayMode = indexDisplayMode
        self.currentPageIndex = currentPageIndex
    }
    
    public init<Model, ViewType: View>(
        items: [Model],
        indexDisplayMode: PageTabViewStyle.IndexDisplayMode = .automatic,
        currentPageIndex: Binding<Int>,
        pageBuilder: (Model) -> ViewType) {
        self.pages = items.map { AnyView(pageBuilder($0)) }
        self.indexDisplayMode = indexDisplayMode
        self.currentPageIndex = currentPageIndex
    }
    
    private let pages: [AnyView]
    private let indexDisplayMode: PageTabViewStyle.IndexDisplayMode
    private var currentPageIndex: Binding<Int>
    
    public var body: some View {
        TabView(selection: currentPageIndex) {
            ForEach(Array(pages.enumerated()), id: \.offset) {
                $0.element.tag($0.offset)
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: indexDisplayMode))
    }
}
Ferree answered 11/10, 2020 at 21:58 Comment(0)
J
1

first you adds the package https://github.com/xmartlabs/PagerTabStripView then

import SwiftUI
import PagerTabStripView

struct MyPagerView: View {

    var body: some View {
   
        PagerTabStripView() {
            FirstView()
                .frame(width: UIScreen.main.bounds.width)
                .pagerTabItem {
                    TitleNavBarItem(title: "ACCOUNT", systomIcon: "character.bubble.fill")
                       
                }
            ContentView()
                .frame(width: UIScreen.main.bounds.width)
                .pagerTabItem {
                    TitleNavBarItem(title: "PROFILE", systomIcon: "person.circle.fill")
                }
         
              NewsAPIView()
                .frame(width: UIScreen.main.bounds.width)
                    .pagerTabItem {
                        TitleNavBarItem(title: "PASSWORD", systomIcon: "lock.fill")
                    }   
        }   
        .pagerTabStripViewStyle(.barButton(indicatorBarHeight: 4, indicatorBarColor: .black, tabItemSpacing: 0, tabItemHeight: 90))
          
 }
   
}

struct TitleNavBarItem: View {
    let title: String
   let systomIcon: String
    var body: some View {
        VStack {
            Image(systemName: systomIcon)
             .foregroundColor( .white)
             .font(.title)
    
             Text( title)
                .font(.system(size: 22))
                .bold()
                .foregroundColor(.white)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.orange)
    }
}
Justice answered 7/9, 2022 at 8:52 Comment(0)
A
-1

The easiest way to do this is via iPages.

import SwiftUI
import iPages

struct ContentView: View {
    @State var currentPage = 0
    var body: some View {
        iPages(currentPage: $currentPage) {
            Text("😋")
            Color.pink
        }
    }
}
Augury answered 22/10, 2020 at 22:45 Comment(1)
That is Open source, but isn't FREE.Onetoone

© 2022 - 2024 — McMap. All rights reserved.