swiftswiftuipaginationuitoolbartabview

How can I make a toolbar with page indicators in SwiftUI like the Weather App?


In SwiftUI, I am trying to place page Indicators on top of a bottom toolbar, but have not come to a resolution.

Paging Indicators

Paging Indicators

Right now, I have a tabview that organizes Views 1-7 horizontally, but the page indicators are on its own island at the bottom of the screen:

TabView {
            View1()
            View2()
            View3()
            View4()
            View5()
            View6()
            View7()
        }
        .tabViewStyle(PageTabViewStyle())
        .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))

I am trying to place the indicators on top of a toolbar with other buttons like how the Apple Weather App has done it:

Apple Weather App

Apple Weather App

I have also tried using a NavigationView with the .toolbar(ToolbarItemGroup) modifier, but that has not worked for me either.

Please let me know if you can help me with this. Thanks


Solution

  • You can do this by wrapping a UIPageControl in a UIViewRepresentable, and then overlay that over your TabView using a ZStack or a .overlay modifier. You'll want to use .tabViewStyle(.page(indexDisplayMode: .never)) to prevent the tab view from displaying its own page control.

    Here's a wrapper for UIPageControl.

    struct PageControl: UIViewRepresentable {
        @Binding var currentPage: Int
        var numberOfPages: Int
        
        func makeCoordinator() -> Coordinator {
            return Coordinator(currentPage: $currentPage)
        }
        
        func makeUIView(context: Context) -> UIPageControl {
            let control = UIPageControl()
            control.numberOfPages = 1
            control.setIndicatorImage(UIImage(systemName: "location.fill"), forPage: 0)
            control.pageIndicatorTintColor = UIColor(.primary)
            control.currentPageIndicatorTintColor = UIColor(.accentColor)        
            control.translatesAutoresizingMaskIntoConstraints = false
            control.setContentHuggingPriority(.required, for: .horizontal)
            control.addTarget(
                context.coordinator,
                action: #selector(Coordinator.pageControlDidFire(_:)),
                for: .valueChanged)
            return control
        }
        
        func updateUIView(_ control: UIPageControl, context: Context) {
            context.coordinator.currentPage = $currentPage
            control.numberOfPages = numberOfPages
            control.currentPage = currentPage
        }
        
        class Coordinator {
            var currentPage: Binding<Int>
            
            init(currentPage: Binding<Int>) {
                self.currentPage = currentPage
            }
            
            @objc
            func pageControlDidFire(_ control: UIPageControl) {
                currentPage.wrappedValue = control.currentPage            
            }
        }
    }
    

    And here's an example of how to use it:

    struct ContentView: View {
        @State var page = 0
        var locations = ["Current Location", "San Francisco", "Chicago", "New York", "London"]
        
        var body: some View {
            ZStack(alignment: .bottom) {
                tabView
                
                VStack {
                    Spacer()
                    controlBar.padding()
                    Spacer().frame(height: 60)
                }
            }
        }
        
        @ViewBuilder
        private var tabView: some View {
            TabView(selection: $page) {
                ForEach(locations.indices, id: \.self) { i in
                    WeatherPage(location: locations[i])
                        .tag(i)
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
        }
        
        @ViewBuilder
        private var controlBar: some View {
            HStack {
                Image(systemName: "map")
                Spacer()
                PageControl(
                    currentPage: $page,
                    numberOfPages: locations.count
                )
                Spacer()
                Image(systemName: "list.bullet")
            }
        }
    }
    
    struct WeatherPage: View {
        var location: String
        
        var body: some View {
            VStack {
                Spacer()
                Text("Weather in \(location)")
                Spacer()
            }
        }
    }