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