TabView with "PageTabViewStyle" does not update it's content when @State var changes
Asked Answered
M

2

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

Milling answered 20/8, 2020 at 6:49 Comment(0)
S
31

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 !!
            }
        }
    }
}
Serpens answered 20/8, 2020 at 7:16 Comment(3)
Thanks. I'll edit the answer so it's a bit easier to see the change.Garboil
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?Strobotron
Adding selection: ... breaks everything. I think there's an underlying bug in the TabView when using selection and the PageTabViewStyle...Alain
J
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 !!!!

Josephinajosephine answered 29/8, 2023 at 3:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.