SwiftUI 2 introduced a new TabView
style called PageTabViewStyle
that enables developers to easily create horizontal Pagers (aka Paging) with dots at the bottom to show users where they are. This is the equivalent of UIPageViewController
from UIKit.
Today, we will cover how to use the new style for TabView
and how to create a custom IndexView
component.
Collection of animations
Getting Started
Using the new API is as simple as setting the new PageTabViewStyle
:
struct ContentView: View {
// MARK: - Private Properties
@State private var currentIndex = 0
private let colors: [Color] = [.red, .blue, .green, .yellow]
// MARK: - Body
var body: some View {
TabView(selection: $currentIndex) {
ForEach(0..<colors.count, id: \.self) { index in
colors[index]
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
}
}
Here, I have an array of colors — with each color being a page — and I am also tagging every Color
view with its desired index. The TabView
initializer has a selection
binding that represents the current page.
Collection of animations
A couple of things to keep in mind:
- The paging dots are white and translucent. Therefore, if your background is also white, you won’t see them (tested on iOS/Xcode Beta 3).
- The contents of the
TabView
cannot be drawn over the safe area insets (Tested on iOS/Xcode Beta 3).
Creating a Custom IndexView
Now that we have the basics covered, we can quickly create any custom component to represent the current page and update it accordingly with our currentIndex
state property.
We are going to take a stab at making a more dynamic version of the system dots. This index component will present at most three dots and animate the insertion/removal as you swipe through the pages.
struct Fancy3DotsIndexView: View {
// MARK: - Public Properties
let numberOfPages: Int
let currentIndex: Int
// MARK: - Drawing Constants
private let circleSize: CGFloat = 16
private let circleSpacing: CGFloat = 12
private let primaryColor = Color.white
private let secondaryColor = Color.white.opacity(0.6)
private let smallScale: CGFloat = 0.6
// MARK: - Body
var body: some View {
HStack(spacing: circleSpacing) {
ForEach(0..<numberOfPages) { index in // 1
if shouldShowIndex(index) {
Circle()
.fill(currentIndex == index ? primaryColor : secondaryColor) // 2
.scaleEffect(currentIndex == index ? 1 : smallScale)
.frame(width: circleSize, height: circleSize)
.transition(AnyTransition.opacity.combined(with: .scale)) // 3
.id(index) // 4
}
}
}
}
// MARK: - Private Methods
func shouldShowIndex(_ index: Int) -> Bool {
((currentIndex - 1)...(currentIndex + 1)).contains(index)
}
}
That’s quite a bit of code, so let’s go through it step by step:
ForEach
andshouldShowIndex()
— Display only three dots or less (for the current page and its adjacent pages)..fill()
and.scale()
— Apply different styling to showcase whichCircle
is focused..transition()
— Animate the insertion and removal of eachCircle
with opacity and scale..id(index)
— Create uniqueCircle
instances for each index.
Final Step
Now we just need to update ContentView
to make use of the new Fancy3DotsIndexView
:
- Add an implicit animation for every change that happens to the
currentIndex
binding. - Overlay the new
IndexView
component and plug in the necessary properties.
struct ContentView: View {
// ...
var body: some View {
TabView(selection: $currentIndex.animation()) { // 1
ForEach(0..<colors.count, id: \.self) { index in
colors[index]
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.overlay(Fancy3DotsIndexView(numberOfPages: colors.count, currentIndex: currentIndex)) // 2
}
}
Voilà!
Final product
Conclusion
Making animations in SwiftUI was already insanely easy, and now with the new TabView
style, you can create any kind of index view! Stay tuned for my upcoming articles!
If you are feeling brave enough, I encourage you to try to make your own animations!