11

I came across a weird Issue in SwiftUI. I created a simple View that only holds a Button and a TabView that uses the PageViewStyle. It seems that the TabView does not update it's content correctly depending on the State of the Variable. It seems that the content gets updated somehow but the View wont be updated how I would expect

Here is the Code of my View:

struct ContentView: View {
    @State var numberOfPages: Int = 0
    @State var selectedIndex = 0
    
    var body: some View {
        VStack {
            Text("Tap Me").onTapGesture(count: 1, perform: {
                self.numberOfPages = [2,5,10,15].randomElement()!
                self.selectedIndex = 0
            })
            
            TabView(selection: $selectedIndex){
                ForEach(0..<numberOfPages, id: \.self) { index in
                    Text("\(index)").background(Color.red)
                }
            }
            .frame(height: 300)
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
        }.background(Color.blue)
    }
}

This is how the result looks after tapping the label several Times. The Initial State is no 0 Pages. After you tap i would expect that the content of the TabView changes so all Pages will be scrollable and visible but just the page indicator updates it State for some reason.

enter image description here

Sebastian Boldt
  • 5,283
  • 9
  • 52
  • 64

2 Answers2

30

TabView expects to have container of pages, but you included only one HStack (with own dynamic content), moreover chaining number of pages you have to reset tab view, so here is a fix.

Tested with Xcode 12 / iOS 14

demo

struct ContentView: View {
    @State var numberOfPages: Int = 0

    var body: some View {
        VStack {
            Text("Tap Me").onTapGesture(count: 1, perform: {
                self.numberOfPages = [2,5,10,15].randomElement()!
            })
            if self.numberOfPages != 0 {
                TabView {
                    ForEach(0..<numberOfPages, id: \.self) { index in
                        Text("\(index)").frame(width: 300).background(Color.red)
                    }
                }
                .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic))
                .frame(height: 300)
                .id(numberOfPages)          // << here !!
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks. I'll edit the answer so it's a bit easier to see the change. – Otziii Jan 23 '22 at 22:59
  • My page index view indicator looks ugly. How do I make it look like in your example - just grey dots with the white dot showing the selected page? – Nazar Jul 11 '22 at 22:20
  • 2
    Adding selection: ... breaks everything. I think there's an underlying bug in the TabView when using selection and the PageTabViewStyle... – Peter Suwara Sep 30 '22 at 05:54
0

some of the time if we pass the selectedIndex as a constructor with a tap with a particular tab doesn't change to avoid it using the below code.Tits work from me

import UIKit
import SwiftUI
 struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIHostingController<AnyView>]

@Binding var currentPage: Int

init(views: [AnyView], currentPage: Binding<Int>) {
    self._currentPage = currentPage
    self.controllers = views.map { UIHostingController(rootView: $0) }
}

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) {
    pageViewController.setViewControllers([controllers[currentPage]], direction: .forward, animated: true)
}

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

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(where: { $0 == viewController }) else { return nil }
        if index == 0 { return nil }
        return parent.controllers[index - 1]
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let index = parent.controllers.firstIndex(where: { $0 == viewController }) else { return nil }
        if index + 1 == parent.controllers.count { return nil }
        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(where: { $0 == visibleViewController }) {
            parent.currentPage = index
        }
    }
}}

calling example

  @State  var selectedTab: Int = 0


   PageViewController(
                        views: [AnyView( DemoView1()), AnyView(DemoView2()), AnyView( DemoView3())],
                        currentPage: $selectedTab
                    )

Happy Coding !!!!