SwiftUI view enabling navigation between pages of content, imitating the behaviour of UIPageViewController for iOS and watchOS

Overview

PageView

SwiftUI view enabling page-based navigation, imitating the behaviour of UIPageViewController in iOS.

watchOS screenshow

Why

SwiftUI doesn't have any kind of paging control component, with features similar to UIPageViewController from UIKit. While on iOS this could be solved by wrapping UIPageViewController into UIViewRepresentable, on watchOS horizontal/vertical paging functionality cannot be achieved without using storyboards, which forces developers into using multiple WKHostingControllers.

This package attempts to provide native SwiftUI component for navigation between pages of content.

Installation

Package requires iOS 13, watchOS 6 and Xcode 11.

Swift Package Manager

For Swift Package Manager add the following package to your Package.swift:

.package(url: "https://github.com/fredyshox/PageView.git", .upToNextMajor(from: "1.4.1")),

Carthage

Carthage is also supported, add following line to Cartfile:

github "fredyshox/PageView" ~> 1.4.1

Demo

Demo app for both iOS and watchOS is provided in Examples/ directory.

Usage

import PageView

PageView component is available as HPageView or VPageView depending on scroll direction (horizontal and vertical, respectively). To add paged view with 3 pages use following code:

@State var pageIndex = 0

...

// horizontal axis
HPageView(selectedPage: $pageIndex) {
    SomeCustomView()
    AnotherCustomView()
    AnotherCustomView()
}

// vertical axis
VPageView(selectedPage: $pageIndex) {
    SomeCustomView()
    AnotherCustomView()
    AnotherCustomView()
}

By default PageView fills all the available area, you can constrain it's size using .frame(width:, height:) view modifier.

Selected page binding

Displayed page can be programmatically controled using Binding. For example, you can change it, with animation effect using:

withAnimation {
  // page index is some State property, which binding was passes into PageView
  self.pageIndex = 2
}

Page switch threshold

You can also control minimum distance that needs to be scrolled to switch page, expressed in fraction of page dimension (width or height, depending on axis). This parameter is called pageSwitchThreshold, and must be in range from 0.0 to 1.0.

For iOS the default value is set to 0.3, while on watchOS 0.5.

Theme

Styling of page control component can be customized by passing PageControlTheme. Customizable properties:

  • backgroundColor
  • dotActiveColor: active page dot color
  • dotInactiveColor: inactive page dot color
  • dotSize: size of page dot
  • spacing: spacing between dots
  • padding: padding of page control
  • xOffset: page control x-axis offset, used only in vertical mode
  • yOffset: page control y-axis offset, used only in horizontal mode
  • alignment: alignment of page control component (default: bottom-center in horizontal mode, center-leading in vertical mode)
let theme = PageControlTheme(
    backgroundColor: .white,
    dotActiveColor: .black,
    dotInactiveColor: .gray,
    dotSize: 10.0,
    spacing: 12.0,
    padding: 5.0,
  	xOffset: 8.0,
    yOffset: -8.0,
    alignment: Alignment(horizontal: .trailing, vertical: .top)
)
...
VPageView(theme: theme) {
    ...
}

There is also a built-in PageControlTheme.default style, mimicking UIPageControl appearance.

API

// Horizontal page view
public struct HPageView<Pages>: View where Pages: View {
    public init(
        selectedPage: Binding<Int>,
        pageSwitchThreshold: CGFloat = .defaultSwitchThreshold,
        theme: PageControlTheme = .default,
        @PageViewBuilder builder: () -> PageContainer<Pages>
    )
}

// Vertical page view
public struct VPageView<Pages>: View where Pages: View {
    public init(
        selectedPage: Binding<Int>,
        pageSwitchThreshold: CGFloat = .defaultSwitchThreshold,
        theme: PageControlTheme = .default,
        @PageViewBuilder builder: () -> PageContainer<Pages>
    )
}

public struct PageControlTheme {
    public var backgroundColor: Color
    public var dotActiveColor: Color
    public var dotInactiveColor: Color
    public var dotSize: CGFloat
    public var spacing: CGFloat
    public var padding: CGFloat
    public var xOffset: CGFloat
    public var yOffset: CGFloat
    public var alignment: Alignment?
}

Screenshots

iOS example

HPageView on watchOS

VPageView on watchOS

Comments
  • On watchOS: drag ended animation starts from full previous screen

    On watchOS: drag ended animation starts from full previous screen

    Not sure if this is due to a new version of watchOS, but PageScrollState.swift's line 70: self.pageOffset = 0.0 seems to reset the position of the "previous" page to its origin before doing the animation to the new page. As a result, the swipe to an adjacent page has sort of a hiccup. Moving the statement inside the DispatchQueue.main.async block fixes the issue. Lines 69~76 would change as follows:

           withAnimation(.easeInOut(duration: 0.2)) {
                self.selectedPage = newPage
            }
            
            DispatchQueue.main.async {
                self.pageOffset = 0.0
                self.isGestureActive = false
            }
    

    Even so, swipe "end animations" still do seem a little unnatural, probably because the remaining distance--whatever it might be--is always reached in 0.2 second in an .easeInOut animation, which might benefit from being .easeOut only. Thank you.

    opened by didierburton 3
  • Added new initializer to support a ForEach loop

    Added new initializer to support a ForEach loop

    The only init method available does not support a for each loop as the example below.

    HPageView(selectedPage: $pageIndex) {      
        ForEach(images, id: \.self) { image in
            Image(uiImage: image.uiImage)
                .resizable()
                .scaledToFill()
        }
    }
    

    But this could be improved and supported by a new initializer

    HPageView(selectedPage: $pageIndex, pageCount: images.count) {      
        ForEach(images, id: \.self) { image in
            Image(uiImage: image.uiImage)
                .resizable()
                .scaledToFill()
        }
    }
    
    opened by sverin 2
  • (watchOS) usability issue in HPageView

    (watchOS) usability issue in HPageView

    hey, I'm building an watchOS app using swiftUI and PageView, the first view inside the HPageView has two buttons inside a VStack, and it's almost impossible to swipe from this view to another, because every time that I try to swipe, it's recognized as a button click

    opened by d1l4y 2
  • Support ForEach

    Support ForEach

    When using the HPageView with sperate views, it works as expected:

    HPageView(selectedPage: $pageIndex) {
    	Color.red
    	Color.green
    	Color.blue
    }
    

    But when trying to use ForEach, the PageView looks strange and only has one page indicator dot:

    HPageView(selectedPage: $pageIndex, theme: pageTheme) {
    	ForEach(0..<2) { n in
    		Text("Number \(n)")
    	}
    }
    
    Bildschirmfoto 2021-06-01 um 15 35 41
    opened by melgu 1
  • Issue with tapping on Views inside of HPageView

    Issue with tapping on Views inside of HPageView

    When I to use a tap gesture on a view inside the HPageView nothing is happening. I am assuming it is because the drag gesture is taking this over. Have you seen this issue? Is there a way to fix this?

    opened by gtgaitoKu 1
  • Change navigationBarTitle on page switch

    Change navigationBarTitle on page switch

    Hi, is there a way to animate/change the .navigationBarTitle on page switch? Currently, it takes the title of the first page and displays that on all the other pages as well.

    opened by kushsolitary 1
  • Release 1.5.0

    Release 1.5.0

    • ForEach-style init (#17)
    • Fix drag animation (#12)
    • Ability to choose drag gesture type using PageGestureType
    • Bumped deployment target to iOS 14 and watchOS 7

    Some of changes/features here are based on work of @gregcotten (#13)

    opened by fredyshox 0
  • Performance improvement

    Performance improvement

    Performance improvement by removing AnyView/Group components causing scrolling not being smooth on slower devices like Apple Watch.

    • [x] AnyView-less implementation
    • [x] refined API with automatic view counting using custom ViewBuilder
    • [x] update README
    opened by fredyshox 0
  • Not support dynamic list data

    Not support dynamic list data

    It works perfectly if we have a fixed length of data. Unfortunately, it does not support dynamic data. Here is a demo

    // viewModel.state.list is fetched after fetchNextPageIfPossible
    VPageView(selectedPage: $pageIndex) {
                ForEach(viewModel.state.list, id: \.self) { _ in
                    ImageCell(urlStr: Image.fullScreenDemo)
                }
            }
            .onAppear {
                viewModel.reset()
                viewModel.fetchNextPageIfPossible()
            }
     .background(Color(hex: "2A3A3F"))
    
    opened by skywalkerlw 0
  • Page snapping not working

    Page snapping not working

    I may have misunderstood what's happening here, but hopefully a video demo will help show what the issue is. Essentially when I swipe on a page, I can see the next page, but when I stop swiping the pages just stay in the half-transitioned state instead of snapping to either the new or current page based on the threshold.

    import PageView
    import SwiftUI
    
    public struct ImageCarouselView: View {
        let imageURLs: [URL]
    
        @State private var index: Int = 0
    
        public var body: some View {
            HPageView(selectedPage: $index, data: imageURLs) { imageURL in
                CachedImage(imageURL) // Kingfisher image view
                // Text(imageURL.absoluteString) // Same problem
            }
        }
    }
    
    // For the purposes of demonstration code
    extension URL: Identifiable {
        var id: String { absoluteString }
    }
    
    ImageCarouselView(imageURLs: imageURLs)
        .frame(width: width, height: width * 4 / 3)
    

    As far as I can tell from the docs this should work as-is. I've tried passing each of the different pageGestureTypes to the constructor, and also tried some extreme settings for pageSwitchThreshold (0.1 and 0.9), but neither of these seems to have any effect.

    I thought that there could be some issue with an interactor above this component in the view hierarchy, but there's nothing obvious that stands out to me, and I'm not quite sure what to look for. This view originally had an onTapGesture handler on it, but I removed that to narrow down the problem.

    Here's a video of the issue. It's from the simulator for ease of recording, but the same behaviour happens on a real device.

    https://user-images.githubusercontent.com/202400/138448200-131f5301-4195-4678-85a7-d31b493e4353.mov

    iOS: 14.5, Xcode: 13.0. PageView: 1.5.0

    Happy to provide any more information that may help diagnose this!

    opened by danpalmer 5
  • Constraining HPageView width to its child view

    Constraining HPageView width to its child view

    In the following scenario I can see all three pages (ImageView) at once and the pages even slide behind "MyLabel". Is there a way to stop that from happening?

    HStack{
        Text("MyLabel: ")
        HPageView(selectedPage: $pageIndex) {
            Image1View()
            Image2View()
            Image3View()
        }
    }
    
    opened by transat 1
  • Is it possible to lock the scrolling?

    Is it possible to lock the scrolling?

    I have pages with knobs and sliders and am unable to select them! Would it be possible to...

    • allow for gestures on pages to override the scrolling gesture?
    • have the option of locking the page scrolling?
    • have the option of only allowing scrolling through the page control ?
    opened by transat 0
  • Improved customization

    Improved customization

    Hi there! Thanks for making this.

    I wanted to customize the page views a bit more so I added:

    1. Modernize PageScrollState to be stored as a @StateObject, bumping requirement to iOS 14 and watchOS 7.
    2. Make a PageViewSettings struct to encapsulate:
    • Existing preference for switchThreshold
    • Preference to dial in an "drag edge threshold" which is expressed as a ratio of the width or height of the page. The default value is 0.5, which allows the entire screen.
    • Preference for changing gesture types (.standard, .simultaneous, .highPriority, AND disable drag events entirely)
    1. Make a default PageViewSettings that matches the current behavior of the page view
    opened by gregcotten 0
Releases(1.5.0)
  • 1.5.0(Sep 30, 2021)

    • ForEach-style init (Support ForEach #17)
    • Fix drag animation (On watchOS: drag ended animation starts from full previous screen #12)
    • Ability to choose drag gesture type using PageGestureType
    • PageScrollState is @StateObject instead of @ObservableObject, which prevents memory leaks
    • Bumped deployment target to iOS 14 and watchOS 7

    Some of changes/features here are based on work of @gregcotten in #13

    Source code(tar.gz)
    Source code(zip)
  • 1.4.1(Jul 15, 2020)

    Changed framework's bundle identifier, which caused inability to run apps on device when using Carthage (https://github.com/Carthage/Carthage/issues/2559#issuecomment-539665460)

    Source code(tar.gz)
    Source code(zip)
  • 1.4.0(Jun 18, 2020)

    New features:

    • ability to programmatically control selected page using selectedPage Binding
    • pageSwitchThreshold parameter, which control distance that needs to be scrolled, to qualify as page switch
    Source code(tar.gz)
    Source code(zip)
  • 1.3.3(Jun 18, 2020)

  • 1.3.2(May 26, 2020)

    Fixed issue with PageControlTheme.offset not being used.

    API changes:

    • PageControlTheme.offset -> xOffset for vertical mode & yOffset for horizontal mode
    Source code(tar.gz)
    Source code(zip)
  • 1.3.1(May 14, 2020)

  • 1.3.0(Mar 20, 2020)

    Performance improvement by removing AnyView/Group components causing scrolling not being smooth on slower devices like Apple Watch.

    • New API: HPageView and VPageView according to chosen scroll direction
    • Ability to specify pages using function builder syntax style
    Source code(tar.gz)
    Source code(zip)
  • 1.2.0(Feb 17, 2020)

  • 1.1.0(Feb 15, 2020)

Owner
Kacper Rączy
iOS developer
Kacper Rączy
A SwiftUI Library for creating resizable partitions for View Content.

Partition Kit Recently Featured In Top 10 Trending Android and iOS Libraries in October and in 5 iOS libraries to enhance your app! What is PartitionK

Kieran Brown 230 Oct 27, 2022
A SwiftUI ScrollView that only scrolls if the content doesn't fit in the View

ScrollViewIfNeeded A SwiftUI ScrollView that only scrolls if the content doesn't fit in the View Installation Requirements iOS 13+ Swift Package Manag

Daniel Klöck 19 Dec 28, 2022
A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView and iPhone X safe area support for content reloading. Built for iOS 10 and later.

Arale A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView support for reloading your content. Built f

Putra Z. 43 Feb 4, 2022
A controller that uses a UIStackView and view controller composition to display content in a list

StackViewController Overview StackViewController is a Swift framework that simplifies the process of building forms and other static content using UIS

Seed 867 Dec 27, 2022
A nice iOS View Capture Swift Library which can capture all content.

SwViewCapture A nice iOS View Capture Library which can capture all content. SwViewCapture could convert all content of UIWebView to a UIImage. 一个用起来还

Xing Chen 597 Nov 22, 2022
A library, which adds the ability to hide navigation bar when view controller is pushed via hidesNavigationBarWhenPushed flag

HidesNavigationBarWhenPushed A library, which adds the ability to hide navigation bar when view controller is pushed via hidesNavigationBarWhenPushed

Danil Gontovnik 55 Oct 19, 2022
SheetPresentation for SwiftUI. Multiple devices support: iOS, watchOS, tvOS, macOS, macCatalyst.

SheetPresentation for SwiftUI. Multiple devices support: iOS, watchOS, tvOS, macOS, macCatalyst.

Aben 13 Nov 17, 2021
Placeholder views based on content, loading, error or empty states

StatefulViewController A protocol to enable UIViewControllers or UIViews to present placeholder views based on content, loading, error or empty states

Alexander Schuch 2.1k Dec 8, 2022
A paging scroll view for SwiftUI, using internal SwiftUI components

PagingView A paging scroll view for SwiftUI, using internal SwiftUI components. This is basically the same as TabView in the paging mode with the inde

Eric Lewis 18 Dec 25, 2022
SwiftUI-Margin adds a margin() viewModifier to a SwiftUI view.

SwiftUI-Margin adds a margin() viewModifier to a SwiftUI view. You will be able to layout the margins in a CSS/Flutter-like.

Masaaki Kakimoto(柿本匡章) 2 Jul 14, 2022
A way to quickly add a notification badge icon to any view. Make any view of a full-fledged animated notification center.

BadgeHub A way to quickly add a notification badge icon to any view. Demo/Example For demo: $ pod try BadgeHub To run the example project, clone the r

Jogendra 772 Dec 28, 2022
Confetti View lets you create a magnificent confetti view in your app

ConfettiView Confetti View lets you create a magnificent confetti view in your app. This was inspired by House Party app's login screen. Written in Sw

Or Ron 234 Nov 22, 2022
A set of UIKit helpers that simplify the usage of UIKit view's and controller's in SwiftUI.

A set of UIKit helpers that simplify the usage of UIKit view's and controller's in SwiftUI. Many of these helpers are useful even in a pure UIKit project.

SwiftUI+ 6 Oct 28, 2022
Advanced List View for SwiftUI with pagination & different states

AdvancedList This package provides a wrapper view around the SwiftUI List view which adds pagination (through my ListPagination package) and an empty,

Chris 246 Jan 3, 2023
📖 A lightweight, paging view solution for SwiftUI

Getting Started | Customization | Installation Getting Started Basic usage Using Pages is as easy as: import Pages struct WelcomeView: View { @S

Nacho Navarro 411 Dec 29, 2022
🚀 Elegant Pager View fully written in pure SwiftUI.

PagerTabStripView Made with ❤️ by Xmartlabs team. XLPagerTabStrip for SwiftUI! Introduction PagerTabStripView is the first pager view built in pure Sw

xmartlabs 482 Jan 9, 2023
⬆️ A SwiftUI view component sliding in from bottom

⬆️ A SwiftUI view component sliding in from bottom

Tieda 595 Dec 28, 2022
Creating a simple selectable tag view in SwiftUI is quite a challenge. here is a simple & elegant example of it.

SwiftUI TagView Creating a simple selectable tag view in SwiftUI is quite a challenge. here is a simple & elegant example of it. Usage: Just copy the

Ahmadreza 16 Dec 28, 2022