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

Time:01-17

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

CodePudding user response:

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()
        }
    }
}
  • Related