Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I implement PageView in SwiftUI?

Tags:

ios

swift

swiftui

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.

like image 285
Azhagusundaram Tamil Avatar asked Oct 15 '19 05:10

Azhagusundaram Tamil


People also ask

How do I create a page view in SwiftUI?

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 .

Can I mix UIKit with SwiftUI?

SwiftUI works seamlessly with the existing UI frameworks on all Apple platforms. For example, you can place UIKit views and view controllers inside SwiftUI views, and vice versa.

How do I create a custom view in SwiftUI?

To get started, you'll create a new custom view to manage your map. Choose File > New > File, select iOS as the platform, select the “SwiftUI View” template, and click Next. Name the new file MapView. swift and click Create.

Is there a native equivalent of uipageviewcontroller in SwiftUI 2?

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.

How do I use a UIKit view in SwiftUI?

To use a UIKit view in SwiftUI, you wrap the view with the UIViewRepresentable protocol. Basically, you just need to create a struct in SwiftUI that adopts the protocol to create and manage a UIView object. In the code above, we create a WebView struct adopts the UIViewRepresentable protocol and implement the required methods.

What is the use of @viewbuilder in SwiftUI?

It allows us to store the state in the parent view and react to page changes. We also use @ViewBuilder for the content closure. @ViewBuilder enables encapsulation of the presentation logic by keeping content descriptions outside the view. It is a pretty popular technique for any container view in SwiftUI.

Is there a way to implement a pager in SwiftUI?

One of the elements that are not available in SwiftUI is a Pager. If you googled “Pager in SwiftUI” you’d see that most people’s solution is to wrap a UIPageViewController into a UIViewControllerRepresentable . Is there any other solution out there? Has no one implemented something similar but with SwiftUI components? SwiftUIPager to the rescue!


3 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:

  • 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<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)
                }
            }
        }
    }
}
like image 98
pawello2222 Avatar answered Sep 30 '22 04:09

pawello2222


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)
like image 31
Farhan Amjad Avatar answered Sep 29 '22 04:09

Farhan Amjad


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))
    }
}
like image 2
Daniel Saidi Avatar answered Sep 29 '22 04:09

Daniel Saidi