Swipeable TabView in SwiftUI
What I want
While testing a swiftUI app I've been working on, I realized I constantly tried to use swipe gestures to switch the active TabView tab. It just feels like such a natural user experience that I was surprised there weren't any good examples with the latest swiftUI. So, here we are :)
Starting from a basic TabView
We start off by creating a barebones swiftUI TabView app with two tabs. By default these can be switched by tapping on their corresponding tab items.
struct ContentView: View {
var body: some View {
TabView{
NavigationView{
Text("Hello, World!")
}.tabItem {
Image(systemName: "house")
Text("Home")
}
NavigationView{
Text("Salut, tout le monde!")
}.tabItem {
Image(systemName: "timelapse")
Text("Space")
}
}
}
}
Using a binding to represent active tab
This is great, but we want to be able to programmatically change the selected
tab. We accomplish this by introducing a state variable to represent the
selected tab. Note the @State
decoration which enables us to us it as a
binding in the TabView
, which tell swiftUI to “tie” the variable with the UI,
and thus trigger re-draws when it changes.
At this point we’ve retained the same functionality, but given ourselves a way to programmatically change the selected tab (by modifying the state variable)
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab){
NavigationView{
Text("Hello, World!")
}.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
NavigationView{
Text("Salut, tout le monde!")
}.tabItem {
Image(systemName: "timelapse")
Text("Space")
}.tag(1)
}
}
Adding a drag gesture handler
Next, we want to be able to detect when the user swipes. A swipe is actually a drag gesture.
So we add a
highPriorityGesture
,
which means it will take higher precedence than any existing gesture listeners
within the contained view. We listen to the onEnded
of a
DragGesture
.
This will give us an event containing the translation amount in width and height.
We are interesting in horizontal swiping so we’ll want to inspect the translation width to see what kind of values we get
TabView(selection: $selectedTab){
NavigationView{
Text("Hello, World!")
}.tabItem {
Image(systemName: "house")
Text("Home")
}.tag(0)
.highPriorityGesture(DragGesture().onEnded({ self.handleSwipe(translation: $0.translation.width)}))
NavigationView{
Text("Salut, tout le monde!")
}.tabItem {
Image(systemName: "timelapse")
Text("Space")
}.tag(1)
.highPriorityGesture(DragGesture().onEnded({ self.handleSwipe(translation: $0.translation.width)}))
}
private func handleSwipe(translation: CGFloat) {
print("handling swipe! horizontal translation was \(translation)")
}
Using a listener to detect swipe and change tab
After playing around with drag gestures and getting a feel for the magnitude of the translation widths, we want to establish a cutoff after which we will consider the swipe as intended to switch tabs. 50 seems like a reasonable number to me :)
let minDragTranslationForSwipe: CGFloat = 50
Now we’ll want to change our swipe handler to see if a given swipe is greater (a swipe to the right), or less than the negative cutoff (a swipe to the left).
Additionally, we’ll want to make sure there are more tabs available to make sure we don’t go past our total number of tabs.
let numTabs = 2
private func handleSwipe(translation: CGFloat) {
if translation > minDragTranslationForSwipe && selectedTab > 0 {
selectedTab -= 1
} else if translation < -minDragTranslationForSwipe && selectedTab < numTabs-1 {
selectedTab += 1
}
}
Final product
And voilà !
Now we have a fully native swipe gesture comptabile TabView! We can extend this
as we add more tabs, by changing the numTabs
constant and making sure we add
appropriate .tag()
to the tabs.
Here is the complete final code: