FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

SOLVED: How do I append a new page into TabView, PageTabViewStyle?

Forums > SwiftUI

I'm using the new PageTabViewStyle for a TabView composed of views created from a ForEach array. The array is created with @State. When the view loads, it shows the views as expected. What I would like to do is append a new page to the TabView by appending an element to the @State array. When I try it, an index indicator is indeed added to the TabView, but I can't swipe to the new page. Tapping the index indicator throws an error of "Invalid IndexPath". The ultimate goal is to use this for a calendar to swipe between months, and on swipe, append the next month to the end of the TabView. Has anyone else been able to do something like this? Or is there just a better way to do this that I don't know?

import SwiftUI

struct TabViewTest: View {

    @State private var arr = [1, 2, 3]

    var body: some View {
        ZStack {
            Color.red.edgesIgnoringSafeArea(.all)

            VStack {
                Button("Add 4") { self.arr.append(4) }

                TabView {
                    ForEach(self.arr, id: \.self) { number in
                        Text("\(number)")
                    }
                }.tabViewStyle(PageTabViewStyle())
            }
        }
    }
}

struct TabViewTest_Previews: PreviewProvider {
    static var previews: some View {
        TabViewTest()
    }
}

1      

Ok, so this captured my curiosity. I thought it would be a good idea to try a plain Jane tabView and see how that works with a dynamic data source. The following code was inspired by @twostraws with his article of a similar vein.How to create views in a loop using ForEach

Essentially, the tabView behaves as expected and propagates tabs across the bottom of the view corresponding to the number of taps. Additionally, corresponding content text and tab labels are applied.

When the .tabViewStyle(PageTabViewStyle()) is uncommented, haywire is a word that comes to mind - the content text disappears, but the "indexDisplay" increments happily as if it anxious to show its invisible content.

In conclusion, this would appear to be a bug. But, perhaps there is some cryptic but well documented code out there that makes it all work like a dream...

struct Tab: Identifiable {
    let id = UUID()
    let title: String
    let label: String
    let tag: Int
}

struct ContentView: View {
    @State private var tabs = [Tab]()
    @State private var count = 1

    var body: some View {
        NavigationView {
            VStack {
                ForEach(tabs) { tab in
                    Text(tab.title)
                }

                TabView {
                    ForEach(tabs) { tab in
                        Text(tab.title).tabItem { Text(tab.label) }.tag(tab.tag)
                    }
                }//.tabViewStyle(PageTabViewStyle())

            }.navigationTitle("Dynamic Data Source")
            .toolbar {
                ToolbarItem {
                    Button("Add", action: addItem)
                }
            }
        }
    }

    func addItem() {
        tabs.append(Tab(title:"Tab Content \(count)", label: "Tab Label \(count)", tag: count))
        self.count += 1
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .preferredColorScheme(.dark)
    }
}

1      

Thanks for taking a look. And yes, haywire is a good word for it. You can tell by the index that it's trying to do it, and is partly successful, but then falls down somewhere along the line. I'm definitely hoping it's a bug and that it will end up being possible. It seems like a reasonable thing for SwiftUI to be able to do.

   

No problem. I took the liberty to file a bug report and will update you if a reply is recieved.

1      

Just wanted to add my vote that any sort of dynamic behavior with PageTabViewStyle just doesn't seem to be working. The best way I can describe it is that the backing view doesn't seem to get updated properly with the number of subviews, causing all sorts of weird behavior. It's hard to point to one specific thing, because it's just all kind of broken.

I also filed feedback. Crossing fingers for a fix in the next beta.

1      

It looks like adding an id(_:) unique to the TabView's content will force it to reload.

See this StackOverflow answer: https://stackoverflow.com/a/63500070/1751778

I think this behaviour might actually be intentional to prevent the TabView from resetting to page 0 every time there is an update to the view's state. This way the tab view will only reset when the content actually changes.

You can probably use the selection binding to create some form of page persistence between content updates.

1      

@avario Adding the id totally worked. I added a UUID @State variable and had it recalculate using UUID() when the select page changes. It allows swiping, it adds and removes the views I want, and everything loads correctly. I should be able to use this as my infinite scroll for a calendar. Brilliant, thanks.

1      

I have a similar problem but unfortunately I did not succeed with your solution.

In next simplified version, my TabView displays a single Tab to ask users to log in, and provides extra tabs when authenticated. Users can log as green user, in which case you get a set of green tabs, or as a blue user, who gets a set of blue tabs. The last tab allows each user to logoff and return to the login page.

Same as @Will-1-Am, it works when .tabViewStyle(PageTabViewStyle()) is commented. However, when I uncomment the tabViewStyle modifier, the TabView does not move from the login page despite the tab indexes are being updated. I tried @avario  suggestion setting a new UUID() as id on creating each tab when login/logoff but no results

import SwiftUI

struct Tab {
    let id:UUID = UUID()
    let tag:Int
    let view:AnyView
}

struct ContentView: View {
    @EnvironmentObject var pageManager:PageManager

    var body: some View {
        TabView(selection: self.$pageManager.selectedTab) {
            ForEach(self.pageManager.tabs, id: \.id) { tab in
                tab.view
                    .tabItem {}.tag(tab.tag)
            }
        }.tabViewStyle(PageTabViewStyle())
    }
}

struct PageView: View {
    @EnvironmentObject var pageManager:PageManager
    var tag: Int
    var title:String
    var body: some View {
        List() {
            if self.pageManager.user != nil {
                Text(self.title)
                    .font(.title)
                    .listRowBackground(self.pageManager.user == "green" ? Color.green : Color.blue)

                Text("user: \(self.pageManager.user!)")
                    .listRowBackground(self.pageManager.user == "green" ? Color.green : Color.blue)

                if self.tag != 1 {
                    Button(action:{self.pageManager.selectedTab = 1}){
                        Text("First Tab")
                    }
                }
                if self.tag != 2 {
                    Button(action:{self.pageManager.selectedTab = 2}){
                        Text("Second Tab")
                    }
                }

                if self.tag != 3 {
                    Button(action:{self.pageManager.selectedTab = 3}){
                        Text("Third Tab")
                    }
                }

                Button(action:{self.pageManager.selectedTab = 4}){
                    Text("Logoff Tab")
                }
            }
        }
    }
}

struct LoginView: View {
    @EnvironmentObject var pageManager:PageManager
    var body: some View {
        List() {
            Text("user: \(self.pageManager.user ?? "not logged")")
            Text("tabs: \(self.pageManager.tabs.count)")
            Text("selected tag: \(self.pageManager.selectedTab)")
            Button(action:{self.pageManager.login(user: "green")}){Text("Log Green user")}
                .listRowBackground(Color.green)
            Button(action:{self.pageManager.login(user: "blue")}){Text("Log Blue user")}
                .listRowBackground(Color.blue)
            ForEach(self.pageManager.tabs, id: \.id) { tab in
                VStack(alignment: .leading) {
                    Text("Tab tag: \(tab.tag)")
                    Text("Tab id:\(tab.id.uuidString)")
                }
            }
        }
    }
}

struct LogoffView: View {
    @EnvironmentObject var pageManager:PageManager
    var body: some View {
        List() {
            if self.pageManager.user != nil {
                Text("Logoff Tab")
                    .font(.title)
                    .listRowBackground(self.pageManager.user == "green" ? Color.green : Color.blue)
                Text("user: \(self.pageManager.user!)")
                    .listRowBackground(self.pageManager.user == "green" ? Color.green : Color.blue)
                Button(action:{self.pageManager.selectedTab = 1}){
                    Text("First Tab")
                }
                Button(action:{self.pageManager.selectedTab = 2}){
                    Text("Second Tab")
                }
                Button(action:{self.pageManager.selectedTab = 3}){
                    Text("Third Tab")
                }
                Button(action:{self.pageManager.logoff()}){Text("Logoff user")}
            }
        }
    }
}

class PageManager:ObservableObject {
    @Published var user:String?
    @Published var selectedTab: Int = 0
    var tabs = [Tab]()

    init() {
        self.loadTabs(user:nil)
    }

    func login(user: String) {
        loadTabs(user: user)
        self.user = user
        self.selectedTab = self.tabs.first!.tag
    }

    func logoff() {
        loadTabs(user: nil)
        self.user = nil
    }

    func loadTabs(user:String?) {
        self.tabs = [Tab]()
        if user == nil {
            self.tabs.append(Tab(tag: 0,view: AnyView(LoginView())))
        } else {
            self.tabs.append(Tab(tag:1, view: AnyView(PageView(tag: 1, title: "First Tab"))))
            self.tabs.append(Tab(tag:2, view: AnyView(PageView(tag: 2, title: "Second Tab"))))
            self.tabs.append(Tab(tag:3, view: AnyView(PageView(tag: 3, title: "Third Tab"))))
            self.tabs.append(Tab(tag:4, view: AnyView(LogoffView())))
        }
    }
}

   

Hacking with Swift is sponsored by Stream

SPONSORED Stream’s latest iOS Chat SDK release provides a better developer experience with new docs, customizable attachments, and UI components, and under-the-hood performance improvements.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.