import SwiftUI

// PillPreference - allows us to track child view position from parent view.
struct PillPreferenceData {
    let index: Int
    let bounds: Anchor<CGRect>
}

struct PillPreferenceKey: PreferenceKey {
    typealias Value = [PillPreferenceData]
    
    static var defaultValue: [PillPreferenceData] = []
    
    static func reduce(value: inout [PillPreferenceData], nextValue: () -> [PillPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

// Just a simple Tab view with some text and an image
struct TabContent: View {
    var image: String
    var text: String
    var body: some View {
        HStack {
            VStack {
                Text(image)
                    .font(.system(size: 60))
                    .frame(width: 100, height: 100)
                    .background(Color(UIColor.systemTeal))
                    .cornerRadius(1000)
                Text(text)
                    .fontWeight(.bold)
                    .padding(.top, 20)
                    
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.black.opacity(0.2))
            .cornerRadius(25)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding(15)
        
    }
}

// View for individual items in a tab controller
struct Pill: View {
    @Binding var index: Int
    @Binding var tapped: Bool
    var key: Int
    var label: String

    var body: some View {
        Text(label)
            .fontWeight(.medium)
            .foregroundColor(.white)
            .padding(.vertical, 10)
            .padding(.horizontal, 32)
            .clipShape(Capsule())
            .anchorPreference(key: PillPreferenceKey.self, value: .bounds, transform: { [PillPreferenceData(index: self.key, bounds: $0)] })
            .onTapGesture {
                self.index = key
                self.tapped = true
                
            }
    }
}

// Parent View that ties everything together
struct SlideTabsView: View {
    @State private var index: Int = 0
    @State private var tabsIndex: Int = 0
    @State private var tapped: Bool = false
    
    var body: some View {
        ZStack {
            TabView(selection: self.$tabsIndex) {
                TabContent(image: "👋", text: "Tab One").tag(0)
                TabContent(image: "👐", text: "Tab Two").tag(1)
                TabContent(image: "😺", text: "Tab Three").tag(2)
            }
            .animation(.default)
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))

            VStack {
                
                Spacer()

                ZStack {
                    HStack(spacing: 0) {
                        Pill(index: self.$index, tapped: $tapped, key: 0, label: "One")
                        Spacer()
                        Pill(index: self.$index, tapped: $tapped, key: 1, label: "Two")
                        Spacer()
                        Pill(index: self.$index, tapped: $tapped, key: 2, label: "Three")
                    }
                    .padding(.vertical, 8)
                    .padding(.horizontal, 8)
                    .background(Color.black.opacity(0.2))
                    .cornerRadius(80)
                    .padding(.horizontal, 16)
                    .backgroundPreferenceValue(PillPreferenceKey.self) { preferences in
                        GeometryReader { geometry in
                            self.createBackground(geometry, preferences)
                                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                        }
                    }
                }.padding(.bottom, 30)
            }
        }
        .onChange(of: index) { _ in
            if self.index != self.tabsIndex {
                self.tabsIndex = self.index
            }
        }
        .onChange(of: tabsIndex) { _ in
            if !self.tapped {
                self.index = self.tabsIndex
            }
        }
        .onChange(of: tapped) { _ in
            // This function makes sure that after Pill is tapped it is not affected by
            // tabsIndex animation
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                self.tapped = false
            }
        }
    }
    
    // Creates movable background for Pills
    func createBackground(_ geometry: GeometryProxy, _ preferences: [PillPreferenceData]) -> some View {
        let p = preferences.first(where: { $0.index == self.index })
        
        let bounds = p != nil ? geometry[p!.bounds] : .zero
                
        return RoundedRectangle(cornerRadius: 80)
            .foregroundColor(Color.blue)
            .frame(width: bounds.size.width, height: bounds.size.height)
            .fixedSize()
            .offset(x: bounds.minX, y: bounds.minY)
            .animation(.linear(duration: 0.2))
    }
}

Custom Tabs view with animated controller

by Kane
This component was inspired by @Guthr_nj's twitter post.

Essentially this is a standard tabs view with custom animated controls.

To accomplish this, component uses Preferences modifier that allows parent view to determine where on the screen child views are located and through that position animated background in a correct place.