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.
-
Try using `UICollectionView`. Here's a great tutorial: https://www.youtube.com/watch?v=a5yjOMLBfSc – Bartosz Kunat Oct 15 '19 at 06:20
-
3[SwiftUIX](https://github.com/SwiftUIX/) has a SwiftUI wrapper for `UIPageViewController` - see [PaginatedViewsContent.swift](https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intramodular/Paging/PaginatedViewsContent.swift). – Yonat Oct 15 '19 at 07:27
-
Please check out [this](https://stackoverflow.com/questions/57028165/swiftui-scrollview-how-to-modify-content-offset-aka-paging/58302447#58302447) . It is pure SwiftUI, so I found the lifecycle easier to manage. Also, you can write any custom SwiftUI code in that. – gujci Oct 15 '19 at 08:46
-
For the pager, check out out [this](https://gist.github.com/Gujci/9154323a1eaf555d718120767ce9ce1d) – gujci Oct 15 '19 at 08:51
7 Answers
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:
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)
}
}
}
}
}

- 46,897
- 22
- 145
- 209
-
-
1
-
1This works amazingly well, thank you! Strange that they have not created a separate view type for this, since it's not semantically a tab view. – Daniel Saidi Oct 11 '20 at 07:40
-
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]`. – JDev Jan 27 '21 at 18:16
-
@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. – pawello2222 Feb 04 '21 at 22:47
-
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) – iKK Apr 07 '21 at 06:06
-
-
-
There is a big disadvantage in that implementation - it's not lazy like UIpageviewcontroller – evya May 25 '23 at 07:30
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)

- 59
- 6

- 754
- 1
- 7
- 15
-
1can please tell me how can I pass multiple views in PageView([/*Prepare a swiftUI View and pass it here.*/]) I try but getting error – Ruchi Makadia Nov 14 '19 at 11:13
-
@FarhanAmjad If I add an `if` statement before or after the page control, something like `if currentPage == 1 { ... }` it breaks pagination. I created a question for this here: https://stackoverflow.com/questions/59448446/conditional-sibling-of-a-pageview-uipageviewcontroller-breaks-paging - any help would be appreciated, thanks! – taber Dec 23 '19 at 20:19
-
Thanks for great answer, I call such like PageView([Text("Test 1") , Text("Test 2")]).frame(height: 250) But swape not working, Can you help me – Md Tariqul Islam Apr 01 '20 at 04:33
-
What does the `vm.Albums` part look like? Is it data that you're passing into the PageView? – Brody Higby Jun 03 '20 at 19:40
-
-
Would you know how to move to the next page from a button action in CardView? – Bhuvan Bhatt Dec 30 '20 at 11:30
-
-
-
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. – CMash Sep 22 '22 at 11:56
-
Note: There's an extra opening brace in the `setViewControllers` trailing closure, in `updateUIViewController` that breaks compilation. – CMash Sep 22 '22 at 12:00
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))
]
)
}
}

- 46,897
- 22
- 145
- 209
-
Am I right to assume this won't be accepted in an App Store application? – iSpain17 Mar 10 '21 at 19:47
-
@iSpain17 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?](https://stackoverflow.com/q/2842357/8697793) – pawello2222 Mar 10 '21 at 20:02
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))
}
}

- 6,079
- 4
- 27
- 29
-
'pageBuilder($0).any()' -> Value of type 'ViewType' has no member 'any' – Brandon A Jan 08 '21 at 19:22
-
1Sorry, I missed to replace it. I have adjusted the answer. `.any()` just a custom view extension that converts any view to `AnyView`. – Daniel Saidi Jan 10 '21 at 11:41
-
Yep I figured it out after managing it a bit. I instead just supply the pages in the init statement and discard the builder block. Thanks for the update. This was a great solution. – Brandon A Jan 11 '21 at 09:57
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()
}
}

- 31
- 2
- 6
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)
}
}

- 29,388
- 11
- 94
- 103

- 1
- 2
- 22
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
}
}
}

- 59
- 6